feat: first implementation of help command, wip
continuous-integration/drone/push Build is passing Details

pull/3/head
Davide Polonio 2023-04-04 17:48:27 +02:00
parent 00bac97e59
commit c4db3be4cb
22 changed files with 526 additions and 125 deletions

View File

@ -36,22 +36,22 @@ curl -F "url=https://example.com/api/tg" \
Build is achieved through Maven. To build a `jar` run: Build is achieved through Maven. To build a `jar` run:
```shell ```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. auto-startup via systemd or openrc units.
## Developing ## Developing
### Automatic testing ### 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 ### Manual testing
For a manual approach, just open a terminal and type `mvn jooby:run`. Assuming you have a database locally available 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 can develop and see live changes of your Mezzotre on the fly. Finally, by using Postman, you can simulate incoming
Telegram events. Telegram events.

View File

@ -41,6 +41,7 @@
<jsonschema2pojo.version>1.1.1</jsonschema2pojo.version> <jsonschema2pojo.version>1.1.1</jsonschema2pojo.version>
<jackson-databind.version>2.13.3</jackson-databind.version> <jackson-databind.version>2.13.3</jackson-databind.version>
<junit-jupiter-params.version>5.9.1</junit-jupiter-params.version> <junit-jupiter-params.version>5.9.1</junit-jupiter-params.version>
<google-guice.version>5.1.0</google-guice.version>
</properties> </properties>
<dependencies> <dependencies>
@ -86,6 +87,12 @@
<artifactId>jooby-guice</artifactId> <artifactId>jooby-guice</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>${google-guice.version}</version>
</dependency>
<dependency> <dependency>
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId> <artifactId>swagger-annotations</artifactId>

View File

@ -3,9 +3,14 @@ package com.github.polpetta.mezzotre;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import io.jooby.Jooby; import io.jooby.Jooby;
import javax.inject.Named;
import javax.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
public class InjectionModule extends AbstractModule { 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; private final Jooby jooby;
public InjectionModule(Jooby jooby) { public InjectionModule(Jooby jooby) {
@ -16,4 +21,11 @@ public class InjectionModule extends AbstractModule {
public Logger getLogger() { public Logger getLogger() {
return jooby.getLog(); return jooby.getLog();
} }
@Named("applicationName")
@Singleton
@Provides
public String getAppName() {
return APPLICATION_NAME;
}
} }

View File

@ -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<VelocityContext> 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);
}
}

View File

@ -1,6 +1,6 @@
package com.github.polpetta.mezzotre.telegram.callbackquery; 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.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.query.QTgChat; 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.BaseRequest;
import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendMessage; 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.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -24,9 +21,6 @@ import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; 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; import org.slf4j.Logger;
/** /**
@ -41,7 +35,7 @@ public class SelectLanguageTutorial implements Processor {
public static final String EVENT_NAME = "selectLanguageTutorial"; public static final String EVENT_NAME = "selectLanguageTutorial";
private final Executor threadPool; private final Executor threadPool;
private final LocalizedMessageFactory localizedMessageFactory; private final TemplateContentGenerator templateContentGenerator;
private final Logger log; private final Logger log;
private final UUIDGenerator uuidGenerator; private final UUIDGenerator uuidGenerator;
@ -89,11 +83,11 @@ public class SelectLanguageTutorial implements Processor {
@Inject @Inject
public SelectLanguageTutorial( public SelectLanguageTutorial(
@Named("eventThreadPool") Executor threadPool, @Named("eventThreadPool") Executor threadPool,
LocalizedMessageFactory localizedMessageFactory, TemplateContentGenerator templateContentGenerator,
Logger log, Logger log,
UUIDGenerator uuidGenerator) { UUIDGenerator uuidGenerator) {
this.threadPool = threadPool; this.threadPool = threadPool;
this.localizedMessageFactory = localizedMessageFactory; this.templateContentGenerator = templateContentGenerator;
this.log = log; this.log = log;
this.uuidGenerator = uuidGenerator; 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 // If we are here then we're sure there is at least a chat associated with this callback
tgChat -> { tgChat -> {
final String message = final String message =
Try.of( templateContentGenerator.mergeTemplate(
() -> { velocityContext ->
final Locale locale = Locale.forLanguageTag(tgChat.getLocale()); velocityContext.put("hasHelpBeenShown", tgChat.getHasHelpBeenShown()),
final ToolManager toolManager = tgChat.getLocale(),
localizedMessageFactory.createVelocityToolManager(locale); "/template/telegram/selectLanguageTutorial.vm");
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();
log.trace("SelectLanguageTutorial event - message to send back: " + message); log.trace("SelectLanguageTutorial event - message to send back: " + message);
@ -193,9 +166,7 @@ public class SelectLanguageTutorial implements Processor {
if (!tgChat.getHasHelpBeenShown()) { if (!tgChat.getHasHelpBeenShown()) {
// Add a button to show all the possible commands // Add a button to show all the possible commands
final String showMeTutorialString = final String showMeTutorialString =
localizedMessageFactory templateContentGenerator.getString(tgChat.getLocale(), "button.showMeTutorial");
.createResourceBundle(Locale.forLanguageTag(tgChat.getLocale()))
.getString("button.showMeTutorial");
final CallbackQueryMetadata callbackQueryMetadata = final CallbackQueryMetadata callbackQueryMetadata =
new CallbackQueryMetadata.CallbackQueryMetadataBuilder() new CallbackQueryMetadata.CallbackQueryMetadataBuilder()

View File

@ -5,16 +5,21 @@ import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutoria
import com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp; import com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import java.util.Set; import java.util.HashMap;
import java.util.Map;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
public class CallbackQuery extends AbstractModule { public class CallbackQuery extends AbstractModule {
@Provides @Provides
@Singleton @Singleton
@Named("eventProcessors") @Named("eventProcessors")
public Set<Processor> getEventProcessor( public Map<String, Processor> getEventProcessor(
SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) { SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) {
return Set.of(selectLanguageTutorial, showHelp); final HashMap<String, Processor> commandMap = new HashMap<>();
commandMap.put(selectLanguageTutorial.getEventName(), selectLanguageTutorial);
commandMap.put(showHelp.getEventName(), showHelp);
return commandMap;
} }
} }

View File

@ -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<String, Processor> tgCommandProcessors;
@Inject
public Help(
TemplateContentGenerator templateContentGenerator,
@Named("eventThreadPool") Executor threadPool,
Clock clock,
@Named("commandProcessor") Map<String, Processor> tgCommandProcessors) {
this.templateContentGenerator = templateContentGenerator;
this.threadPool = threadPool;
this.clock = clock;
this.tgCommandProcessors = tgCommandProcessors;
}
@Override
public Set<String> getTriggerKeywords() {
return Set.of(TRIGGERING_STAGING_NAME);
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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);
}
}

View File

@ -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<String> getTriggerKeywords() {
return Collections.emptySet();
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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")));
}
}

View File

@ -0,0 +1,5 @@
package com.github.polpetta.mezzotre.telegram.command;
public interface NotFoundFactory {
NotFound create(String commandName);
}

View File

@ -4,6 +4,7 @@ import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture; 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} * @return a {@link String} providing the keyword to trigger the current {@link Processor}
*/ */
String getTriggerKeyword(); Set<String> getTriggerKeywords();
/** /**
* Process the current update * Process the current update
@ -31,4 +32,17 @@ public interface Processor {
* @return a {@link CompletableFuture} with the result of the computation * @return a {@link CompletableFuture} with the result of the computation
*/ */
CompletableFuture<Optional<BaseRequest<?, ?>>> process(TgChat chat, Update update); CompletableFuture<Optional<BaseRequest<?, ?>>> 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";
}
} }

View File

@ -5,8 +5,8 @@ import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.inject.Named; import javax.inject.Named;
@ -21,15 +21,18 @@ import javax.inject.Named;
@Singleton @Singleton
public class Router { public class Router {
private final Set<Processor> tgCommandProcessors; private final Map<String, Processor> tgCommandProcessors;
private final Executor threadPool; private final Executor threadPool;
private final NotFoundFactory notFoundFactory;
@Inject @Inject
public Router( public Router(
@Named("commandProcessor") Set<Processor> tgCommandProcessors, @Named("commandProcessor") Map<String, Processor> tgCommandProcessors,
@Named("eventThreadPool") Executor threadPool) { @Named("eventThreadPool") Executor threadPool,
NotFoundFactory notFoundFactory) {
this.tgCommandProcessors = tgCommandProcessors; this.tgCommandProcessors = tgCommandProcessors;
this.threadPool = threadPool; this.threadPool = threadPool;
this.notFoundFactory = notFoundFactory;
} }
/** /**
@ -58,15 +61,13 @@ public class Router {
.map(list -> list[0]) .map(list -> list[0])
.filter(wannabeCommand -> wannabeCommand.startsWith("/")) .filter(wannabeCommand -> wannabeCommand.startsWith("/"))
.or(() -> Optional.ofNullable(chat.getChatContext().getStage())) .or(() -> Optional.ofNullable(chat.getChatContext().getStage()))
.flatMap( .map(
command -> command ->
tgCommandProcessors.stream() tgCommandProcessors
// FIXME this is fucking stupid, why iterate over, just use a map! .getOrDefault(command, notFoundFactory.create(command))
// Make mapping at startup then we're gucci for the rest of the run .process(chat, update))
.filter(ex -> ex.getTriggerKeyword().equals(command)) // This should never happen
.findAny()) .orElse(CompletableFuture.failedFuture(new IllegalStateException())),
.map(executor -> executor.process(chat, update))
.orElse(CompletableFuture.failedFuture(new CommandNotFoundException())),
threadPool); threadPool);
} }
} }

View File

@ -1,6 +1,6 @@
package com.github.polpetta.mezzotre.telegram.command; 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.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial; 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.model.request.ParseMode;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
import io.vavr.control.Try;
import java.nio.charset.StandardCharsets;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; 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; import org.slf4j.Logger;
/** /**
@ -37,29 +33,35 @@ import org.slf4j.Logger;
@Singleton @Singleton
public class Start implements Processor { public class Start implements Processor {
private static final String TRIGGERING_STAGING_NAME = "/start";
private final Executor threadPool; private final Executor threadPool;
private final Logger log; private final Logger log;
private final UUIDGenerator uuidGenerator; private final UUIDGenerator uuidGenerator;
private final Clock clock; private final Clock clock;
private final LocalizedMessageFactory localizedMessageFactory; private final String applicationName;
private final TemplateContentGenerator templateContentGenerator;
@Inject @Inject
public Start( public Start(
LocalizedMessageFactory localizedMessageFactory, TemplateContentGenerator templateContentGenerator,
@Named("eventThreadPool") Executor threadPool, @Named("eventThreadPool") Executor threadPool,
Logger log, Logger log,
UUIDGenerator uuidGenerator, UUIDGenerator uuidGenerator,
Clock clock) { Clock clock,
this.localizedMessageFactory = localizedMessageFactory; @Named("applicationName") String applicationName) {
this.templateContentGenerator = templateContentGenerator;
this.threadPool = threadPool; this.threadPool = threadPool;
this.log = log; this.log = log;
this.uuidGenerator = uuidGenerator; this.uuidGenerator = uuidGenerator;
this.clock = clock; this.clock = clock;
this.applicationName = applicationName;
} }
@Override @Override
public String getTriggerKeyword() { public Set<String> getTriggerKeywords() {
return "/start"; return Set.of(TRIGGERING_STAGING_NAME);
} }
@Override @Override
@ -73,36 +75,19 @@ public class Start implements Processor {
3 - Reply to Telegram 3 - Reply to Telegram
*/ */
final String message = final String message =
Try.of( templateContentGenerator.mergeTemplate(
() -> { velocityContext -> {
final Locale locale = Locale.forLanguageTag(chat.getLocale()); velocityContext.put("firstName", update.message().chat().firstName());
final ToolManager toolManager = // FIXME add some very cool markdown formatter instead of concatenating stuff
localizedMessageFactory.createVelocityToolManager(locale); // this way
final VelocityContext context = velocityContext.put("programName", "_" + applicationName + "_");
new VelocityContext(toolManager.createContext()); },
context.put("firstName", update.message().chat().firstName()); chat.getLocale(),
context.put("programName", "_Mezzotre_"); "template/telegram/start.vm");
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();
log.trace("Start command - message to send back: " + message); log.trace("Start command - message to send back: " + message);
final ChatContext chatContext = chat.getChatContext(); final ChatContext chatContext = chat.getChatContext();
chatContext.setStage(getTriggerKeyword()); chatContext.setStage(TRIGGERING_STAGING_NAME);
chatContext.setStep(0); chatContext.setStep(0);
chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now());
chat.setChatContext(chatContext); chat.setChatContext(chatContext);
@ -135,13 +120,9 @@ public class Start implements Processor {
.build()); .build());
final String englishButton = final String englishButton =
localizedMessageFactory templateContentGenerator.getString(Locale.US, "changeLanguage.english");
.createResourceBundle(Locale.US)
.getString("changeLanguage.english");
final String italianButton = final String italianButton =
localizedMessageFactory templateContentGenerator.getString(Locale.ITALY, "changeLanguage.italian");
.createResourceBundle(Locale.ITALY)
.getString("changeLanguage.italian");
final SendMessage messageToSend = final SendMessage messageToSend =
new SendMessage(chat.getId(), message) new SendMessage(chat.getId(), message)

View File

@ -1,10 +1,11 @@
package com.github.polpetta.mezzotre.telegram.command.di; package com.github.polpetta.mezzotre.telegram.command.di;
import com.github.polpetta.mezzotre.telegram.command.Processor; import com.github.polpetta.mezzotre.telegram.command.*;
import com.github.polpetta.mezzotre.telegram.command.Start;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; 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.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.app.VelocityEngine;
@ -12,11 +13,37 @@ import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
public class Command extends AbstractModule { 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<String, Processor> mapForProcessor(Processor processor) {
final HashMap<String, Processor> commandMap = new HashMap<>();
processor
.getTriggerKeywords()
.forEach(
keyword -> {
commandMap.put(keyword, processor);
});
return commandMap;
}
@Provides @Provides
@Singleton @Singleton
@Named("commandProcessor") @Named("commandProcessor")
public Set<Processor> getCommandProcessor(Start start) { public Map<String, Processor> getCommandProcessor(Start start, Help help) {
return Set.of(start); final HashMap<String, Processor> commandMap = new HashMap<>();
commandMap.putAll(mapForProcessor(start));
commandMap.putAll(mapForProcessor(help));
return commandMap;
} }
@Provides @Provides

View File

@ -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 "";
}
}

View File

@ -1,11 +1,16 @@
start.helloFirstName=Hello {0}! \ud83d\udc4b 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 language down below \ud83d\udc47
start.cmdDescription=Trigger this very bot
selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* 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.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.instructions=You can always change your language settings by typing /selectLanguageTutorial in the chat.
changeLanguage.english=English changeLanguage.english=English
changeLanguage.italian=Italian changeLanguage.italian=Italian
changeLanguage.cmdDescription=Select the new language I will use to speak to you
spell.speakWithAnimals=Speak with animals spell.speakWithAnimals=Speak with animals
button.showMeTutorial=Show me what you can do! 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.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.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

View File

@ -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}

View File

@ -6,6 +6,7 @@ import static org.mockito.Mockito.*;
import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.Loader;
import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.helper.TestConfig;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; 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.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.TgChat; 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.QCallbackQueryContext;
@ -67,7 +68,8 @@ class SelectLanguageTutorialIntegrationTest {
selectLanguageTutorial = selectLanguageTutorial =
new SelectLanguageTutorial( new SelectLanguageTutorial(
Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor(),
new LocalizedMessageFactory(Loader.defaultVelocityEngine()), new TemplateContentGenerator(
new LocalizedMessageFactory(Loader.defaultVelocityEngine())),
LoggerFactory.getLogger(SelectLanguageTutorial.class), LoggerFactory.getLogger(SelectLanguageTutorial.class),
fakeUUIDGenerator); fakeUUIDGenerator);
} }

View File

@ -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<String, Processor> commands = new HashMap<>();
final Processor dummy1 =
new Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/a", "/b");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return null;
}
@Override
public String getLocaleDescriptionKeyword() {
return "start.cmdDescription";
}
};
final Processor dummy2 =
new Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/different");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotFuture = help.process(tgChat, update);
final Optional<BaseRequest<?, ?>> 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());
}
}

View File

@ -11,6 +11,7 @@ import com.google.gson.Gson;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -32,12 +33,12 @@ class RouterTest {
gson = new Gson(); gson = new Gson();
dummyEmptyExampleProcessor = mock(Processor.class); dummyEmptyExampleProcessor = mock(Processor.class);
when(dummyEmptyExampleProcessor.getTriggerKeyword()).thenReturn("/example"); when(dummyEmptyExampleProcessor.getTriggerKeywords()).thenReturn(Set.of("/example"));
when(dummyEmptyExampleProcessor.process(any(), any())) when(dummyEmptyExampleProcessor.process(any(), any()))
.thenReturn(CompletableFuture.completedFuture(Optional.empty())); .thenReturn(CompletableFuture.completedFuture(Optional.empty()));
anotherKeyWithResultProcessor = mock(Processor.class); anotherKeyWithResultProcessor = mock(Processor.class);
when(anotherKeyWithResultProcessor.getTriggerKeyword()).thenReturn("/anotherExample"); when(anotherKeyWithResultProcessor.getTriggerKeywords()).thenReturn(Set.of("/anotherExample"));
when(anotherKeyWithResultProcessor.process(any(), any())) when(anotherKeyWithResultProcessor.process(any(), any()))
.thenReturn( .thenReturn(
CompletableFuture.completedFuture(Optional.of(new SendMessage(1234L, "hello world")))); CompletableFuture.completedFuture(Optional.of(new SendMessage(1234L, "hello world"))));
@ -46,7 +47,10 @@ class RouterTest {
@Test @Test
void shouldMessageExampleMessageAndGetEmptyOptional() throws Exception { void shouldMessageExampleMessageAndGetEmptyOptional() throws Exception {
final Router router = 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); final TgChat fakeChat = mock(TgChat.class);
when(fakeChat.getChatContext()).thenReturn(new ChatContext()); when(fakeChat.getChatContext()).thenReturn(new ChatContext());
final Update update = final Update update =
@ -85,8 +89,13 @@ class RouterTest {
void shouldSelectRightExecutorAndReturnResult() throws Exception { void shouldSelectRightExecutorAndReturnResult() throws Exception {
final Router router = final Router router =
new Router( new Router(
Set.of(dummyEmptyExampleProcessor, anotherKeyWithResultProcessor), Map.of(
Executors.newSingleThreadExecutor()); "/example",
dummyEmptyExampleProcessor,
"/anotherExample",
anotherKeyWithResultProcessor),
Executors.newSingleThreadExecutor(),
mock(NotFoundFactory.class));
final TgChat fakeChat = mock(TgChat.class); final TgChat fakeChat = mock(TgChat.class);
when(fakeChat.getChatContext()).thenReturn(new ChatContext()); when(fakeChat.getChatContext()).thenReturn(new ChatContext());
final Update update = final Update update =

View File

@ -6,6 +6,7 @@ import static org.mockito.Mockito.*;
import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.Loader;
import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.helper.TestConfig;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; 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.TgChat;
import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.query.QTgChat; 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.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.apache.velocity.app.VelocityEngine; 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.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
@ -62,12 +61,7 @@ class StartIntegrationTest {
void setUp() throws Exception { void setUp() throws Exception {
database = database =
Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer));
velocityEngine = new VelocityEngine(); velocityEngine = Loader.defaultVelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath");
velocityEngine.setProperty(
"resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
velocityEngine.init();
localizedMessageFactory = new LocalizedMessageFactory(velocityEngine);
final Logger log = LoggerFactory.getLogger(Start.class); final Logger log = LoggerFactory.getLogger(Start.class);
@ -76,11 +70,12 @@ class StartIntegrationTest {
start = start =
new Start( new Start(
localizedMessageFactory, new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)),
Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor(),
log, log,
fakeUUIDGenerator, fakeUUIDGenerator,
fakeClock); fakeClock,
"Mezzotre");
} }
@Test @Test