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();
+ }
+
+ /**
+ * 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);
+ }
+}
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..acbff5d
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java
@@ -0,0 +1,137 @@
+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;
+
+/**
+ * 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<
+ Tuple3>, CompletableFuture>>
+ 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;
+
+ @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 {
+ 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
+ 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();
+ }
+
+ /**
+ * 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 extends TQRootBean, ?>> column) {
+ final CompletableFuture jobExecution = new CompletableFuture<>();
+ 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) {
+ 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..9603653
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java
@@ -0,0 +1,67 @@
+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;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.slf4j.Logger;
+
+/**
+ * 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 {
+
+ 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;
+ }
+
+ /**
+ * 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()
+ */
+ @CanIgnoreReturnValue
+ 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()
+ */
+ @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/di/Db.java b/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java
similarity index 88%
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..62e2d70 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,16 +1,16 @@
-package com.github.polpetta.mezzotre.orm.di;
+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 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/orm/telegram/ChatUtil.java b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java
new file mode 100644
index 0000000..4fc26fd
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java
@@ -0,0 +1,83 @@
+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.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());
+ }
+}
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/CallbackQueryDI.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java
new file mode 100644
index 0000000..34927ed
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java
@@ -0,0 +1,33 @@
+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;
+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")
+ public Map getEventProcessor(
+ SelectLanguageTutorial selectLanguageTutorial, ShowHelp 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/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/Field.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java
new file mode 100644
index 0000000..14d6968
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java
@@ -0,0 +1,47 @@
+package com.github.polpetta.mezzotre.telegram.callbackquery;
+
+/**
+ * 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
+ *
+ * @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;
+ }
+ }
+
+ /**
+ * Enumerator with custom fields for {@link
+ * com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp} callback query
+ */
+ 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/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java
new file mode 100644
index 0000000..901d6f9
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java
@@ -0,0 +1,51 @@
+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;
+import com.pengrad.telegrambot.request.BaseRequest;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+
+/**
+ * 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;
+ private final CallbackQueryContextCleaner callbackQueryContextCleaner;
+ private final String eventName;
+
+ @Inject
+ public NotFound(
+ Logger logger,
+ CallbackQueryContextCleaner callbackQueryContextCleaner,
+ @Assisted String eventName) {
+ this.log = logger;
+ this.callbackQueryContextCleaner = callbackQueryContextCleaner;
+ 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);
+ callbackQueryContextCleaner.removeGroupAsync(callbackQueryContext.getEntryGroup());
+ 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/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java
index 021a177..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
@@ -17,12 +17,31 @@ 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();
+ /**
+ * 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();
+ }
+
/**
* 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 c52ad0d..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,12 +1,11 @@
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.CallbackQueryContextCleaner;
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.orm.telegram.ChatUtil;
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;
@@ -14,9 +13,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 +20,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,61 +34,26 @@ 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;
-
- /**
- * 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;
- }
- }
+ private final ChatUtil chatUtil;
+ private final CallbackQueryContextCleaner callbackQueryContextCleaner;
@Inject
public SelectLanguageTutorial(
@Named("eventThreadPool") Executor threadPool,
- LocalizedMessageFactory localizedMessageFactory,
+ TemplateContentGenerator templateContentGenerator,
Logger log,
- UUIDGenerator uuidGenerator) {
+ UUIDGenerator uuidGenerator,
+ ChatUtil chatUtil,
+ CallbackQueryContextCleaner callbackQueryContextCleaner) {
this.threadPool = threadPool;
- this.localizedMessageFactory = localizedMessageFactory;
+ this.templateContentGenerator = templateContentGenerator;
this.log = log;
this.uuidGenerator = uuidGenerator;
+ this.chatUtil = chatUtil;
+ this.callbackQueryContextCleaner = callbackQueryContextCleaner;
}
@Override
@@ -108,16 +66,8 @@ 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())
+ chatUtil
+ .extractChat(callbackQueryContext, update)
.map(
tgChat -> {
tgChat.setLocale(
@@ -126,7 +76,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 "
@@ -147,55 +98,34 @@ 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 =
- 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);
- 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 =
- Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
+ final Optional messageId = Util.extractMessageId(update);
BaseRequest, ?> baseRequest;
Optional helpButton = Optional.empty();
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(),
+ "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..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
@@ -1,15 +1,66 @@
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;
+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.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;
-public class ShowHelp implements Processor {
+/**
+ * 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";
+ private final Executor threadPool;
+ private final Help modelHelp;
+ private final Map
+ tgCommandProcessors;
+ private final Map eventProcessor;
+ private final ChatUtil chatUtil;
+ private final CallbackQueryContextCleaner callbackQueryContextCleaner;
+
+ @Inject
+ public ShowHelp(
+ @Named("eventThreadPool") Executor threadPool,
+ com.github.polpetta.mezzotre.telegram.model.Help modelHelp,
+ @Named("commandProcessor")
+ Map tgCommandProcessors,
+ @Named("eventProcessors")
+ 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
public String getEventName() {
@@ -19,8 +70,44 @@ public 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(
+ () ->
+ chatUtil
+ .extractChat(callbackQueryContext, update)
+ .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)
+ .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);
+ 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
new file mode 100644
index 0000000..3637292
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java
@@ -0,0 +1,25 @@
+package com.github.polpetta.mezzotre.telegram.callbackquery;
+
+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 {
+
+ /**
+ * 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/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/callbackquery/di/CallbackQuery.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java
deleted file mode 100644
index 0e252d4..0000000
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.polpetta.mezzotre.telegram.callbackquery.di;
-
-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.Set;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-public class CallbackQuery extends AbstractModule {
- @Provides
- @Singleton
- @Named("eventProcessors")
- public Set getEventProcessor(
- SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) {
- return Set.of(selectLanguageTutorial, showHelp);
- }
-}
diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java
new file mode 100644
index 0000000..767fdab
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java
@@ -0,0 +1,57 @@
+package com.github.polpetta.mezzotre.telegram.command;
+
+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;
+import javax.inject.Singleton;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
+
+public class CommandDI 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 Map getCommandProcessor(Start start, Help help) {
+ final HashMap commandMap = new HashMap<>();
+ commandMap.putAll(mapForProcessor(start));
+ commandMap.putAll(mapForProcessor(help));
+
+ return commandMap;
+ }
+
+ @Provides
+ public VelocityEngine getVelocityEngine() {
+ final VelocityEngine velocityEngine = new VelocityEngine();
+ velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
+ velocityEngine.setProperty(
+ "classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
+ velocityEngine.init();
+ return velocityEngine;
+ }
+}
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..9b05c14
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java
@@ -0,0 +1,85 @@
+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;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+/**
+ * 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";
+
+ private final Executor threadPool;
+ private final Map tgCommandProcessors;
+ private final Map
+ eventProcessor;
+ private final com.github.polpetta.mezzotre.telegram.model.Help modelHelp;
+ private final ChatUtil chatUtil;
+
+ @Inject
+ public Help(
+ @Named("eventThreadPool") Executor threadPool,
+ @Named("commandProcessor") Map tgCommandProcessors,
+ @Named("eventProcessors")
+ Map eventProcessor,
+ 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
+ public Set getTriggerKeywords() {
+ return Set.of(TRIGGERING_STAGING_NAME);
+ }
+
+ @Override
+ public CompletableFuture>> process(TgChat chat, Update update) {
+ 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);
+
+ final InlineKeyboardButton[] buttons =
+ modelHelp.generateInlineKeyBoardButton(chat, eventProcessor);
+
+ if (buttons.length > 0) {
+ sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons));
+ }
+
+ return Optional.of(sendMessage);
+ },
+ 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..c1e3155
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java
@@ -0,0 +1,57 @@
+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;
+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 java.util.concurrent.Executor;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * 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 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,
+ TemplateContentGenerator templateContentGenerator,
+ @Named("eventThreadPool") Executor threadPool) {
+ this.commandName = commandName;
+ this.templateContentGenerator = templateContentGenerator;
+ this.threadPool = threadPool;
+ }
+
+ @Override
+ public Set getTriggerKeywords() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public CompletableFuture>> process(TgChat chat, Update update) {
+ 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/telegram/command/NotFoundFactory.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java
new file mode 100644
index 0000000..381c449
--- /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;
+
+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..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
@@ -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;
/**
@@ -16,12 +17,12 @@ 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}
*/
- 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().getSimpleName().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..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
@@ -1,13 +1,13 @@
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;
-import com.github.polpetta.mezzotre.util.Clock;
+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.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;
@@ -15,17 +15,14 @@ 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.Collections;
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;
/**
@@ -35,31 +32,37 @@ 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 LocalizedMessageFactory localizedMessageFactory;
+ private final String applicationName;
+ private final ChatUtil chatUtil;
+
+ private final TemplateContentGenerator templateContentGenerator;
@Inject
public Start(
- LocalizedMessageFactory localizedMessageFactory,
+ TemplateContentGenerator templateContentGenerator,
@Named("eventThreadPool") Executor threadPool,
Logger log,
UUIDGenerator uuidGenerator,
- Clock clock) {
- this.localizedMessageFactory = localizedMessageFactory;
+ @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
- public String getTriggerKeyword() {
- return "/start";
+ public Set getTriggerKeywords() {
+ return Set.of(TRIGGERING_STAGING_NAME);
}
@Override
@@ -73,40 +76,18 @@ 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.setStep(0);
- chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now());
- chat.setChatContext(chatContext);
- chat.save();
+ 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();
@@ -115,11 +96,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 =
@@ -127,21 +110,19 @@ 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 =
- 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
deleted file mode 100644
index ef0ee6b..0000000
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java
+++ /dev/null
@@ -1,31 +0,0 @@
-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.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import java.util.Set;
-import javax.inject.Named;
-import javax.inject.Singleton;
-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 {
- @Provides
- @Singleton
- @Named("commandProcessor")
- public Set getCommandProcessor(Start start) {
- return Set.of(start);
- }
-
- @Provides
- public VelocityEngine getVelocityEngine() {
- final VelocityEngine velocityEngine = new VelocityEngine();
- velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
- velocityEngine.setProperty(
- "classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
- velocityEngine.init();
- return velocityEngine;
- }
-}
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..2cc8e70
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java
@@ -0,0 +1,104 @@
+package com.github.polpetta.mezzotre.telegram.model;
+
+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.UUIDGenerator;
+import com.github.polpetta.types.json.CallbackQueryMetadata;
+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;
+
+/**
+ * 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 final TemplateContentGenerator templateContentGenerator;
+ private final UUIDGenerator uuidGenerator;
+
+ @Inject
+ public Help(TemplateContentGenerator templateContentGenerator, UUIDGenerator uuidGenerator) {
+ this.templateContentGenerator = templateContentGenerator;
+ 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 -> {
+ 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");
+ }
+
+ /**
+ * 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) {
+ 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/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..f509088
--- /dev/null
+++ b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java
@@ -0,0 +1,86 @@
+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;
+
+/**
+ * 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;
+
+ @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/i18n/message.properties b/src/main/resources/i18n/message.properties
index cce17e2..f8458cd 100644
--- a/src/main/resources/i18n/message.properties
+++ b/src/main/resources/i18n/message.properties
@@ -1,11 +1,23 @@
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
+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.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
+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.
+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 64ca1f3..f8458cd 100644
--- a/src/main/resources/i18n/message_en_US.properties
+++ b/src/main/resources/i18n/message_en_US.properties
@@ -1,11 +1,23 @@
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
+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.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
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.
+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 1c13674..6b49a81 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 /selectLanguageTutorial nella chat.
+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
-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.
+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 1c13674..6b49a81 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 /selectLanguageTutorial nella chat.
+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
-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.
+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/command/start.0.vm b/src/main/resources/template/command/start.0.vm
deleted file mode 100644
index 7889c0b..0000000
--- a/src/main/resources/template/command/start.0.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-**${i18n.start.helloFirstName.insert(${firstName})}**
-
-${i18n.start.description.insert(${programName})}
\ No newline at end of file
diff --git a/src/main/resources/template/telegram/help.vm b/src/main/resources/template/telegram/help.vm
new file mode 100644
index 0000000..0ff5078
--- /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/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/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/telegram/start.vm b/src/main/resources/template/telegram/start.vm
new file mode 100644
index 0000000..3d55cb5
--- /dev/null
+++ b/src/main/resources/template/telegram/start.vm
@@ -0,0 +1,3 @@
+*${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 dc1ebb6..5d6d0c0 100644
--- a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java
+++ b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java
@@ -1,19 +1,23 @@
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.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,13 +54,27 @@ 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() {
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/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/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/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/CallbackQueryContextCleanerIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java
new file mode 100644
index 0000000..afe607a
--- /dev/null
+++ b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java
@@ -0,0 +1,78 @@
+package com.github.polpetta.mezzotre.orm;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 java.time.Duration;
+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 CallbackQueryContextCleanerIntegrationTest {
+
+ @Container
+ private final PostgreSQLContainer> postgresServer =
+ new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE);
+
+ private Database database;
+
+ private BatchBeanCleanerService batchBeanCleanerService;
+ private CallbackQueryContextCleaner callbackQueryContextCleaner;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ database =
+ Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer));
+
+ batchBeanCleanerService =
+ new BatchBeanCleanerService(
+ LoggerFactory.getLogger(BatchBeanCleanerService.class),
+ Pair.of(0, TimeUnit.MILLISECONDS));
+ batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10));
+
+ callbackQueryContextCleaner =
+ new CallbackQueryContextCleaner(
+ 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 {
+ new CallbackQueryContext("doesn't matter", "an id", new CallbackQueryMetadata()).save();
+
+ final Integer got = callbackQueryContextCleaner.removeGroupAsync("an id").get();
+
+ assertEquals(1, got);
+ assertEquals(0, new QCallbackQueryContext().findCount());
+ }
+
+ @Test
+ @Timeout(value = 1, unit = TimeUnit.MINUTES)
+ void shouldDeleteById() throws Exception {
+ new CallbackQueryContext("an id", "doesn't matter", new CallbackQueryMetadata()).save();
+
+ final Integer got = callbackQueryContextCleaner.removeIdAsync("an id").get();
+
+ assertEquals(1, got);
+ assertEquals(0, new QCallbackQueryContext().findCount());
+ }
+}
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/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 a4e53c1..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
@@ -6,10 +6,15 @@ 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.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;
@@ -21,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;
@@ -51,6 +61,8 @@ class SelectLanguageTutorialIntegrationTest {
private Database database;
private SelectLanguageTutorial selectLanguageTutorial;
private UUIDGenerator fakeUUIDGenerator;
+ private ChatUtil chatUtil;
+ private BatchBeanCleanerService batchBeanCleanerService;
@BeforeAll
static void beforeAll() {
@@ -63,19 +75,35 @@ 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(
Executors.newSingleThreadExecutor(),
- new LocalizedMessageFactory(Loader.defaultVelocityEngine()),
+ 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() {
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"
@@ -84,7 +112,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!"
@@ -93,14 +121,14 @@ 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"
+ " 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"
@@ -110,7 +138,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"
@@ -119,19 +147,19 @@ 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),
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"
+ " 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!",
@@ -142,7 +170,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,
@@ -182,7 +210,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 =
@@ -190,8 +219,13 @@ 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 =
- 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"));
@@ -210,14 +244,20 @@ 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());
}
final TgChat retrievedTgChat = new QTgChat().id.eq(tgChatId).findOne();
assertNotNull(retrievedTgChat);
- assertEquals(language.getLocale(), retrievedTgChat.getLocale());
+ 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());
}
@@ -225,7 +265,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,
@@ -297,16 +337,21 @@ 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 =
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 =
- 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"));
@@ -325,14 +370,20 @@ 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());
}
final TgChat retrievedTgChat = new QTgChat().id.eq(tgChatId).findOne();
assertNotNull(retrievedTgChat);
- assertEquals(language.getLocale(), retrievedTgChat.getLocale());
+ 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/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/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java
new file mode 100644
index 0000000..24453e2
--- /dev/null
+++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java
@@ -0,0 +1,259 @@
+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.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;
+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.*;
+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;
+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);
+
+ 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);
+
+ final com.github.polpetta.mezzotre.telegram.model.Help modelHelp =
+ new com.github.polpetta.mezzotre.telegram.model.Help(
+ new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)),
+ new UUIDGenerator());
+
+ help =
+ new Help(
+ Executors.newSingleThreadExecutor(),
+ commands,
+ events,
+ modelHelp,
+ new ChatUtil(fakeClock));
+
+ 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);
+
+ 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("/help", gotChatChatContext.getStage());
+ assertEquals(42, gotChatChatContext.getPreviousMessageUnixTimestampInSeconds());
+ }
+}
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/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());
+ }
+}
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..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
@@ -6,9 +6,11 @@ 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;
+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;
@@ -22,8 +24,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 +62,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 +71,12 @@ class StartIntegrationTest {
start =
new Start(
- localizedMessageFactory,
+ new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)),
Executors.newSingleThreadExecutor(),
log,
fakeUUIDGenerator,
- fakeClock);
+ "Mezzotre",
+ new ChatUtil(fakeClock));
}
@Test
@@ -127,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);
@@ -185,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);
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"));
+ }
+}
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());
+ }
+}