feat: add help message (#3)
continuous-integration/drone Build is passing Details

* create a dynamic help message
* add documentation and add way more tests
* add batch entity cleaning service and add service structure

Co-authored-by: Davide Polonio <poloniodavide@gmail.com>
Reviewed-on: #3
devel
Davide Polonio 2023-04-20 11:30:06 +02:00
parent 00bac97e59
commit 5788d59516
67 changed files with 3717 additions and 367 deletions

10
.mvn/jvm.config Normal file
View File

@ -0,0 +1,10 @@
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

View File

@ -36,22 +36,22 @@ curl -F "url=https://example.com/api/tg" \
Build is achieved through Maven. To build a `jar` run:
```shell
mvn package -DskipTests=true -Dmaven.test.skip=true -Dmaven.site.skip=true -Dmaven.javadoc.skip=true
./mvnw package -DskipTests=true -Dmaven.test.skip=true -Dmaven.site.skip=true -Dmaven.javadoc.skip=true
```
In the `target/` folder wou will find an uber-jar and optionally the possibility to run it via a script and to setup
In the `target/` folder you will find an uber-jar and optionally the possibility to run it via a script and to setup
auto-startup via systemd or openrc units.
## Developing
### Automatic testing
You can simply run tests with `mvn test`. This will run `UT` and `IT` tests together.
You can simply run tests with `./mvnw test`. This will run `UT` and `IT` tests together.
### Manual testing
For a manual approach, just open a terminal and type `mvn jooby:run`. Assuming you have a database locally available
(check out [application.conf](conf/application.conf)) and a valid Telegram token set (maybe as enviroment variable) you
(check out [application.conf](conf/application.conf)) and a valid Telegram token set (maybe as environment variable) you
can develop and see live changes of your Mezzotre on the fly. Finally, by using Postman, you can simulate incoming
Telegram events.

View File

@ -3,6 +3,10 @@
db.url = "jdbc:postgresql://localhost:5433/example"
db.user = example
db.password = example
hikari.autoCommit = false
hikari.maximumPoolSize = 4
telegram.key = akey
application.lang = en en-US it it-IT

23
pom.xml
View File

@ -41,6 +41,8 @@
<jsonschema2pojo.version>1.1.1</jsonschema2pojo.version>
<jackson-databind.version>2.13.3</jackson-databind.version>
<junit-jupiter-params.version>5.9.1</junit-jupiter-params.version>
<google-guice.version>5.1.0</google-guice.version>
<error-prone.version>2.18.0</error-prone.version>
</properties>
<dependencies>
@ -86,6 +88,12 @@
<artifactId>jooby-guice</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>${google-guice.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
@ -233,7 +241,11 @@
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>${error-prone.version}</version>
</dependency>
</dependencies>
<build>
@ -272,12 +284,19 @@
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.2</version>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${error-prone.version}</version>
</path>
<path>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>

View File

@ -1,11 +1,12 @@
package com.github.polpetta.mezzotre;
import com.github.polpetta.mezzotre.orm.di.Db;
import com.github.polpetta.mezzotre.orm.OrmDI;
import com.github.polpetta.mezzotre.route.RouteDI;
import com.github.polpetta.mezzotre.route.Telegram;
import com.github.polpetta.mezzotre.route.di.Route;
import com.github.polpetta.mezzotre.telegram.callbackquery.di.CallbackQuery;
import com.github.polpetta.mezzotre.telegram.command.di.Command;
import com.github.polpetta.mezzotre.util.di.ThreadPool;
import com.github.polpetta.mezzotre.telegram.callbackquery.CallbackQueryDI;
import com.github.polpetta.mezzotre.telegram.command.CommandDI;
import com.github.polpetta.mezzotre.util.ServiceModule;
import com.github.polpetta.mezzotre.util.UtilDI;
import com.google.inject.*;
import com.google.inject.Module;
import com.google.inject.name.Names;
@ -23,11 +24,11 @@ public class App extends Jooby {
public static final Function<Jooby, Collection<Module>> DEFAULT_DI_MODULES =
(jooby) -> {
final HashSet<Module> modules = new HashSet<>();
modules.add(new Db());
modules.add(new ThreadPool());
modules.add(new Route());
modules.add(new Command());
modules.add(new CallbackQuery());
modules.add(new OrmDI());
modules.add(new UtilDI());
modules.add(new RouteDI());
modules.add(new CommandDI());
modules.add(new CallbackQueryDI());
return modules;
};
@ -42,7 +43,7 @@ public class App extends Jooby {
if (modules == null || modules.size() == 0) {
toInject = DEFAULT_DI_MODULES.apply(this);
}
toInject.add(new InjectionModule(this));
toInject.add(new AppDI(this));
toInject.add(new JoobyModule(this));
final Injector injector = Guice.createInjector(runningEnv, toInject);
@ -55,6 +56,7 @@ public class App extends Jooby {
install(
injector.getInstance(Key.get(Extension.class, Names.named("flyWayMigrationExtension"))));
install(injector.getInstance(Key.get(Extension.class, Names.named("ebeanExtension"))));
install(injector.getInstance(Key.get(ServiceModule.class, Names.named("serviceModule"))));
decorator(new AccessLogHandler());
decorator(new TransactionalRequest());

View File

@ -0,0 +1,41 @@
package com.github.polpetta.mezzotre;
import com.github.polpetta.mezzotre.orm.BatchBeanCleanerService;
import com.google.common.util.concurrent.Service;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import io.jooby.Jooby;
import java.util.List;
import javax.inject.Named;
import javax.inject.Singleton;
import org.slf4j.Logger;
public class AppDI extends AbstractModule {
// In the future we can get this name from mvn, by now it is good as it is
public static final String APPLICATION_NAME = "Mezzotre";
private final Jooby jooby;
public AppDI(Jooby jooby) {
this.jooby = jooby;
}
@Provides
public Logger getLogger() {
return jooby.getLog();
}
@Named("applicationName")
@Singleton
@Provides
public String getAppName() {
return APPLICATION_NAME;
}
@Provides
@Singleton
@Named("services")
public List<Service> getApplicationServices(BatchBeanCleanerService batchBeanCleanerService) {
return List.of(batchBeanCleanerService);
}
}

View File

@ -1,19 +0,0 @@
package com.github.polpetta.mezzotre;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import io.jooby.Jooby;
import org.slf4j.Logger;
public class InjectionModule extends AbstractModule {
private final Jooby jooby;
public InjectionModule(Jooby jooby) {
this.jooby = jooby;
}
@Provides
public Logger getLogger() {
return jooby.getLog();
}
}

View File

@ -0,0 +1,86 @@
package com.github.polpetta.mezzotre.i18n;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.function.Consumer;
import javax.inject.Inject;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.util.StringBuilderWriter;
/**
* This class aims to generate localized messages or strings either merging Velocity templates or
* taking directly the keys from {@link java.util.ResourceBundle}
*
* @author Davide Polonio
* @since 1.0
*/
public class TemplateContentGenerator {
private final LocalizedMessageFactory localizedMessageFactory;
@Inject
public TemplateContentGenerator(LocalizedMessageFactory localizedMessageFactory) {
this.localizedMessageFactory = localizedMessageFactory;
}
/**
* Merge a Velocity template located in the local Jar resource path with the given {@link Locale},
* provided as a {@link String}. A {@link Consumer} is provided in order to let the user customize
* the {@link VelocityContext} with additional variables. Localization strings will be directly
* taken via the use of {@link LocalizedTool} class.
*
* @param velocityContextConsumer a lambda function that allows for the customization of {@link
* VelocityContext} instance
* @param localeAsString a {@link Locale} formatted as a {@link String}. Note that {@link
* Locale#forLanguageTag(String)} will be used for the conversion
* @param templateName the path to the local Jar resource
* @return a {@link String} containing the localized message
*/
public String mergeTemplate(
Consumer<VelocityContext> velocityContextConsumer,
String localeAsString,
String templateName) {
final Locale locale = Locale.forLanguageTag(localeAsString);
final ToolManager toolManager = localizedMessageFactory.createVelocityToolManager(locale);
final VelocityContext velocityContext = new VelocityContext(toolManager.createContext());
velocityContextConsumer.accept(velocityContext);
final StringBuilder content = new StringBuilder();
final StringBuilderWriter stringBuilderWriter = new StringBuilderWriter(content);
toolManager
.getVelocityEngine()
.mergeTemplate(
templateName, StandardCharsets.UTF_8.name(), velocityContext, stringBuilderWriter);
stringBuilderWriter.close();
return content.toString();
}
/**
* 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);
}
}

View File

@ -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.
*
* <p>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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
entriesToRemove;
private final Logger log;
private final Pair<Integer, TimeUnit> 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<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>>
deletionListener;
@Inject
public BatchBeanCleanerService(
Logger logger,
@Named("serviceRunningCheckTime") Pair<Integer, TimeUnit> 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<Integer> removeAsync(
String id, PString<? extends TQRootBean<?, ?>> column) {
final CompletableFuture<Integer> 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<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) {
deletionListener.add(listener);
}
/**
* Remove a listener
*
* @param listener the listener to be removed
*/
public void removeListener(
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) {
deletionListener.remove(listener);
}
}

View File

@ -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<PString<QCallbackQueryContext>> ENTRY_GROUP =
() -> new QCallbackQueryContext().entryGroup;
private static final Supplier<PString<QCallbackQueryContext>> 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<Integer> 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<Integer> removeIdAsync(String id) {
log.trace("CallbackQueryContext single entity " + id + " queued for removal");
return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get());
}
}

View File

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

View File

@ -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<String, Object> 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<TgChat> 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());
}
}

View File

@ -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) {

View File

@ -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<String, Processor> getEventProcessor(
SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) {
final HashMap<String, Processor> commandMap = new HashMap<>();
commandMap.put(selectLanguageTutorial.getEventName(), selectLanguageTutorial);
commandMap.put(showHelp.getEventName(), showHelp);
return commandMap;
}
}

View File

@ -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<Processor> tgEventProcessors;
private final Map<String, Processor> tgEventProcessors;
private final Executor threadPool;
private final NotFoundFactory notFoundFactory;
@Inject
public Dispatcher(
@Named("eventProcessors") Set<Processor> tgEventProcessors,
@Named("eventThreadPool") Executor threadPool) {
@Named("eventProcessors") Map<String, Processor> 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);
}

View File

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

View File

@ -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<Optional<BaseRequest<?, ?>>> 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());
}
}

View File

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

View File

@ -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<String> getPrettyPrintLocaleKeyName() {
return Optional.empty();
}
/**
* Process the current event
*

View File

@ -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<Integer> messageId =
Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
final Optional<Integer> messageId = Util.extractMessageId(update);
BaseRequest<?, ?> baseRequest;
Optional<InlineKeyboardMarkup> 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()

View File

@ -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<String, com.github.polpetta.mezzotre.telegram.command.Processor>
tgCommandProcessors;
private final Map<String, Processor> 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<String, com.github.polpetta.mezzotre.telegram.command.Processor> tgCommandProcessors,
@Named("eventProcessors")
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<Optional<BaseRequest<?, ?>>> 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<Integer> 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);
}
}

View File

@ -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<Integer> extractMessageId(Update update) {
return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
}
}

View File

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

View File

@ -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<Processor> getEventProcessor(
SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) {
return Set.of(selectLanguageTutorial, showHelp);
}
}

View File

@ -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<String, Processor> mapForProcessor(Processor processor) {
final HashMap<String, Processor> commandMap = new HashMap<>();
processor
.getTriggerKeywords()
.forEach(
keyword -> {
commandMap.put(keyword, processor);
});
return commandMap;
}
@Provides
@Singleton
@Named("commandProcessor")
public Map<String, Processor> getCommandProcessor(Start start, Help help) {
final HashMap<String, Processor> 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;
}
}

View File

@ -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<String, Processor> tgCommandProcessors;
private final Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor>
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<String, Processor> tgCommandProcessors,
@Named("eventProcessors")
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<String> getTriggerKeywords() {
return Set.of(TRIGGERING_STAGING_NAME);
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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);
}
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.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<String> getTriggerKeywords();
/**
* Process the current update
@ -31,4 +32,17 @@ public interface Processor {
* @return a {@link CompletableFuture} with the result of the computation
*/
CompletableFuture<Optional<BaseRequest<?, ?>>> process(TgChat chat, Update update);
/**
* Provide the key to retrieve the current processor descriptor. This is useful for help messages
* or to provide a user with a description of what this command does. Whilst a default
* implementation is provided, we suggest to rename it accordingly since the name generation is
* based on reflection and can be fragile and subject to code refactor changes.
*
* @return a {@link String} with the name of the localization key containing the command
* description
*/
default String getLocaleDescriptionKeyword() {
return this.getClass().getSimpleName().toLowerCase() + ".cmdDescription";
}
}

View File

@ -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<Processor> tgCommandProcessors;
private final Map<String, Processor> tgCommandProcessors;
private final Executor threadPool;
private final NotFoundFactory notFoundFactory;
@Inject
public Router(
@Named("commandProcessor") Set<Processor> tgCommandProcessors,
@Named("eventThreadPool") Executor threadPool) {
@Named("commandProcessor") Map<String, Processor> 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);
}
}

View File

@ -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<String> 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)

View File

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

View File

@ -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<String, Processor> 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<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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);
}
}

View File

@ -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}.
*
* <p>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 <a href="https://github.com/google/guava/wiki/ServiceExplained">Guava Service Wiki
* explained</a>
* @see ServiceManager
*/
public class ServiceModule implements Extension {
private final ServiceManager serviceManager;
@Inject
public ServiceModule(@Named("services") List<Service> services) {
serviceManager = new ServiceManager(services);
}
@Override
public boolean lateinit() {
return true;
}
@Override
public void install(@NotNull Jooby application) throws Exception {
final CompletableFuture<Void> 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();
}
}

View File

@ -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<Service> moduleList) {
return new ServiceModule(moduleList);
}
@Provides
@Singleton
@Named("serviceRunningCheckTime")
public Pair<Integer, TimeUnit> getTime() {
return Pair.of(5, TimeUnit.SECONDS);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
**${i18n.start.helloFirstName.insert(${firstName})}**
${i18n.start.description.insert(${programName})}

View File

@ -0,0 +1,7 @@
${i18n.help.description}:
#foreach(${command} in ${commands})
-#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})}
#end
${i18n.help.buttonsToo}

View File

@ -0,0 +1,3 @@
${i18n.notfound.description.insert(${cmdName})}
${i18n.notfound.howToHelp}

View File

@ -0,0 +1,3 @@
*${i18n.start.helloFirstName.insert(${firstName})}*
${i18n.start.description.insert(${programName})}

View File

@ -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<Integer, TimeUnit> getTime() {
return Pair.of(0, TimeUnit.MILLISECONDS);
}
}
@Test

View File

@ -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<Service> moduleList) {
return new ServiceModule(moduleList);
}
@Provides
@Singleton
@Named("serviceRunningCheckTime")
public Pair<Integer, TimeUnit> 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));
}

View File

@ -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());

View File

@ -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<VelocityContext> 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<String> getKeys() {
return new Enumeration<String>() {
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);
}
}

View File

@ -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<Integer> 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<Integer> integerCompletableFuture =
batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup);
final CompletableFuture<Integer> integerCompletableFuture2 =
batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup);
final Integer gotDeletion1 = integerCompletableFuture.get();
final Integer gotDeletion2 = integerCompletableFuture2.get();
assertEquals(2, gotDeletion1);
assertEquals(0, gotDeletion2);
}
}

View File

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

View File

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

View File

@ -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<TgChat> 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<TgChat> tgChatOptional =
chatUtil.extractChat(
callbackQueryContext, update); // null just for test purposes, not usually expected
assertEquals(expectedChat, tgChatOptional.get());
}
}

View File

@ -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<ChatContext> 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<ChatContext> chatContextArgumentCaptor =
ArgumentCaptor.forClass(ChatContext.class);
verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture());
final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue();
final Map<String, Object> gotAdditionalProperties =
capturedChatContextValue.getAdditionalProperties();
assertEquals(2, gotAdditionalProperties.size());
assertEquals(obj1, gotAdditionalProperties.get("field1"));
assertEquals(obj2, gotAdditionalProperties.get("field2"));
}
}

View File

@ -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<Optional<BaseRequest<?, ?>>> gotResult =
notFound.process(fakeCallbackQuery, fakeUpdate);
verify(fakeCallbackQueryCleaner, times(1)).removeGroupAsync(eq("123"));
assertEquals(Optional.empty(), gotResult.get());
}
}

View File

@ -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<Arguments> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> processFuture =
selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update);
this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> processFuture =
selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update);
this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
deletionCallback = callBackFuture.get();
assertEquals(entryGroupId, deletionCallback._1());
assertEquals(1, deletionCallback._3().get());
assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount());
}
}

View File

@ -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<String, Processor> eventProcessors,
Map<String, com.github.polpetta.mezzotre.telegram.command.Processor> 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<Arguments> 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<String, com.github.polpetta.mezzotre.telegram.command.Processor> commands =
new HashMap<>();
final com.github.polpetta.mezzotre.telegram.command.Processor dummy1 =
new com.github.polpetta.mezzotre.telegram.command.Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/a", "/b");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return null;
}
@Override
public String getLocaleDescriptionKeyword() {
return "start.cmdDescription";
}
};
final com.github.polpetta.mezzotre.telegram.command.Processor dummy2 =
new com.github.polpetta.mezzotre.telegram.command.Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/different");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return null;
}
@Override
public String getLocaleDescriptionKeyword() {
return "help.cmdDescription";
}
};
commands.put("/a", dummy1);
commands.put("/b", dummy1);
commands.put("/different", dummy2);
final Map<String, Processor> events = new HashMap<>();
final Processor dummyEvent1 =
new Processor() {
@Override
public String getEventName() {
return "exampleEvent";
}
@Override
public boolean canBeDirectlyInvokedByTheUser() {
return true;
}
@Override
public Optional<String> getPrettyPrintLocaleKeyName() {
return Optional.of("changeLanguage.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String> getPrettyPrintLocaleKeyName() {
return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotResultFuture =
showHelp.process(callbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<InlineKeyboardButton> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotResultFuture =
showHelp.process(callbackQueryContext, update);
final Optional<BaseRequest<?, ?>> baseRequestOptional = gotResultFuture.get();
assertDoesNotThrow(baseRequestOptional::get);
final Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
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!");
}
}

View File

@ -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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = gotResponseFuture.get();
final BaseRequest<?, ?> gotResponse = gotResponseOpt.get();
final InlineKeyboardMarkup replyMarkup =
(InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup");
assertNull(replyMarkup);
}
}

View File

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

View File

@ -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<String, Processor> commands = new HashMap<>();
final Processor dummy1 =
new Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/a", "/b");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return null;
}
@Override
public String getLocaleDescriptionKeyword() {
return "start.cmdDescription";
}
};
final Processor dummy2 =
new Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/different");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return null;
}
@Override
public String getLocaleDescriptionKeyword() {
return "help.cmdDescription";
}
};
commands.put("/a", dummy1);
commands.put("/b", dummy1);
commands.put("/different", dummy2);
final Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<String> getPrettyPrintLocaleKeyName() {
return Optional.of("changeLanguage.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String> getPrettyPrintLocaleKeyName() {
return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotFuture = help.process(tgChat, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = gotFuture.get();
final BaseRequest<?, ?> gotResponse = gotResponseOpt.get();
assertInstanceOf(SendMessage.class, gotResponse);
final String message = (String) gotResponse.getParameters().get("text");
assertEquals(
"Here is a list of what I can do:\n"
+ "\n"
+ "- /a /b: Trigger this very bot\n"
+ "- /different: Print the help message\n"
+ "\n"
+ "You can do the same operations you'd do with the commands aforementioned by"
+ " selecting the corresponding button below \uD83D\uDC47",
message);
final InlineKeyboardButton[][] keyboard =
((InlineKeyboardMarkup)
gotResponse
.getParameters()
.getOrDefault("reply_markup", new InlineKeyboardMarkup()))
.inlineKeyboard();
final List<InlineKeyboardButton> 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());
}
}

View File

@ -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<String, Processor> commands = Collections.emptyMap();
final Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
help.process(fakeTgChat, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
help.process(fakeTgChat, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = gotResponseFuture.get();
final BaseRequest<?, ?> gotResponse = gotResponseOpt.get();
final InlineKeyboardMarkup replyMarkup =
(InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup");
assertNull(replyMarkup);
}
}

View File

@ -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<String> getTriggerKeywords() {
return null;
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(TgChat chat, Update update) {
return null;
}
}
final Test test = new Test();
assertEquals("test.cmdDescription", test.getLocaleDescriptionKeyword());
}
}

View File

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

View File

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

View File

@ -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<String> getTriggerKeywords() {
return Set.of("/example", "/another");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String> getPrettyPrintLocaleKeyName() {
return Optional.of("changeLanguage.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
CallbackQueryContext callbackQueryContext, Update update) {
return CompletableFuture.completedFuture(Optional.empty());
}
};
final Processor invisibleProcessor1 =
new Processor() {
@Override
public String getEventName() {
return "invisible";
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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());
}
}

View File

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

View File

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