Merge pull request #668 from addo37/add-replyflow
Add state machine capability to AbilityBot via ReplyFlow
This commit is contained in:
commit
d1f060fb04
@ -8,6 +8,7 @@
|
|||||||
* [[Simple Example]]
|
* [[Simple Example]]
|
||||||
* [[Hello Ability]]
|
* [[Hello Ability]]
|
||||||
* [[Using Replies]]
|
* [[Using Replies]]
|
||||||
|
* [[State Machines]]
|
||||||
* [[Database Handling]]
|
* [[Database Handling]]
|
||||||
* [[Bot Testing]]
|
* [[Bot Testing]]
|
||||||
* [[Bot Recovery]]
|
* [[Bot Recovery]]
|
||||||
|
134
TelegramBots.wiki/abilities/State-Machines.md
Normal file
134
TelegramBots.wiki/abilities/State-Machines.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# State Machines
|
||||||
|
|
||||||
|
AbilityBot supports state machines using ReplyFlows. Internally, they set and transition the state of the user based on their actions so far.
|
||||||
|
Developers may declare this flow control in either a bottom-up or a top-down approach. If you're already familiar with what a `Reply` is, consider ReplyFlows as the cherry on top.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
A ReplyFlow can not be directly instantiated; it must be built. First, let's create
|
||||||
|
some basic replies.
|
||||||
|
```java
|
||||||
|
Reply saidLeft = Reply.of(upd ->
|
||||||
|
silent.send("Sir, I have gone left.", getChatId(upd)),
|
||||||
|
hasMessageWith("left"));
|
||||||
|
|
||||||
|
Reply saidRight = Reply.of(upd ->
|
||||||
|
silent.send("Sir, I have gone right.", getChatId(upd)),
|
||||||
|
hasMessageWith("right"));
|
||||||
|
```
|
||||||
|
The first `Reply` effectively replies to any message that has the text "left". Once such a message is received, the
|
||||||
|
bot replies with "Sir, I have gone left". Likewise, the bot acts for when "right" is encountered.
|
||||||
|
|
||||||
|
What if now, you'd like to protect those two replies behind one more reply? Let's say, the bot first should ask the user to give it directions.
|
||||||
|
This means that people can't tell your bot to turn left or right UNLESS the bot asks for directions. Let's trigger that when the user sends "wake up" to the bot!
|
||||||
|
|
||||||
|
```java
|
||||||
|
// We instantiate a ReplyFlow builder with our internal db (DBContext instance) passed
|
||||||
|
// State is always preserved in the db of the bot and remains even after termination
|
||||||
|
ReplyFlow.builder(db)
|
||||||
|
// Just like replies, a ReplyFlow can take an action, here we want to send a
|
||||||
|
// statement to prompt the user for directions!
|
||||||
|
.action(upd -> silent.send("Command me to go left or right!", getChatId(upd)))
|
||||||
|
// We should only trigger this flow when the user says "wake up"
|
||||||
|
.onlyIf(hasMessageWith("wake up"))
|
||||||
|
// The next method takes in an object of type Reply.
|
||||||
|
// Here we chain our replies together
|
||||||
|
.next(saidLeft)
|
||||||
|
// We chain one more reply, which is when the user commands your bot to go right
|
||||||
|
.next(saidRight)
|
||||||
|
// Finally, we build our ReplyFlow
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
For the sake of completeness, here's the auxiliary method `hasMessageWith`.
|
||||||
|
```java
|
||||||
|
private Predicate<Update> hasMessageWith(String msg) {
|
||||||
|
return upd -> upd.getMessage().getText().equalsIgnoreCase(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
To run this example in your own AbilityBot, just have a method return that ReplyFlow we just built. Yup, it's that easy, just like how you're used to
|
||||||
|
building replies and abilities.
|
||||||
|
## More Complex States
|
||||||
|
Let's say that your bot becomes naughty when the user asks it to go left. We want the bot to say "I don't know how to go left." when the user commands it to go left. We would also like to chain more commands after this state. Here's
|
||||||
|
how that's done.
|
||||||
|
We must create a new ReplyFlow that would be chained to the initial one. Here's what our left flow would look like.
|
||||||
|
```java
|
||||||
|
ReplyFlow leftflow = ReplyFlow.builder(db)
|
||||||
|
.action(upd -> silent.send("I don't know how to go left.", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("left"))
|
||||||
|
.next(saidLeft)
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
And now, saidLeft reply becomes:
|
||||||
|
```java
|
||||||
|
Reply saidLeft = Reply.of(upd -> silent.send("Sir, I have gone left.", getChatId(upd)),
|
||||||
|
hasMessageWith("go left or else"));
|
||||||
|
```
|
||||||
|
Now, after your naughty bot retaliates, the user can say "go left or else" to force the bot to go left. Awesome, our logic now looks like this:
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
![Alt text](./img/replyflow_diagram.svg)
|
||||||
|
<img src="./img/replyflow_diagram.svg">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
```java
|
||||||
|
public static class ReplyFlowBot extends AbilityBot {
|
||||||
|
public class ReplyFlowBot extends AbilityBot {
|
||||||
|
public ReplyFlowBot(String botToken, String botUsername) {
|
||||||
|
super(botToken, botUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int creatorId() {
|
||||||
|
return <YOUR ID HERE>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlow directionFlow() {
|
||||||
|
Reply saidLeft = Reply.of(upd -> silent.send("Sir, I have gone left.", getChatId(upd)),
|
||||||
|
hasMessageWith("go left or else"));
|
||||||
|
|
||||||
|
ReplyFlow leftflow = ReplyFlow.builder(db)
|
||||||
|
.action(upd -> silent.send("I don't know how to go left.", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("left"))
|
||||||
|
.next(saidLeft).build();
|
||||||
|
|
||||||
|
Reply saidRight = Reply.of(upd -> silent.send("Sir, I have gone right.", getChatId(upd)),
|
||||||
|
hasMessageWith("right"));
|
||||||
|
|
||||||
|
return ReplyFlow.builder(db)
|
||||||
|
.action(upd -> silent.send("Command me to go left or right!", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("wake up"))
|
||||||
|
.next(leftflow)
|
||||||
|
.next(saidRight)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Predicate<Update> hasMessageWith(String msg) {
|
||||||
|
return upd -> upd.getMessage().getText().equalsIgnoreCase(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Inline Declaration
|
||||||
|
As you can see in the above example, we used a bottom-up approach. We declared the leaf replies before we got to the root reply flow.
|
||||||
|
If you'd rather have a top-down approach, then you may declare your replies inline to achieve that.
|
||||||
|
|
||||||
|
```java
|
||||||
|
ReplyFlow.builder(db)
|
||||||
|
.action(upd -> silent.send("Command me to go left or right!", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("wake up"))
|
||||||
|
.next(Reply.of(upd ->
|
||||||
|
silent.send("Sir, I have gone left.", getChatId(upd)),
|
||||||
|
hasMessageWith("left")))
|
||||||
|
.next(Reply.of(upd ->
|
||||||
|
silent.send("Sir, I have gone right.", getChatId(upd)),
|
||||||
|
hasMessageWith("right")))
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
## State Consistency
|
||||||
|
Under the hood, AbilityBot will generate integers that represent the state of the instigating user. However,
|
||||||
|
if you add more replies and reply flows, these integers may no longer be consistent. If you'd like to always have consistent state IDs, you
|
||||||
|
should always pass a unique ID to the ReplyFlow builder like so:
|
||||||
|
```java
|
||||||
|
ReplyFlow.builder(db, <ID HERE>)
|
||||||
|
```
|
3
TelegramBots.wiki/abilities/img/replyflow_diagram.svg
Normal file
3
TelegramBots.wiki/abilities/img/replyflow_diagram.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
@ -680,7 +680,8 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
|||||||
Stream<Reply> extensionReplies = extensions.stream()
|
Stream<Reply> extensionReplies = extensions.stream()
|
||||||
.flatMap(ext -> stream(ext.getClass().getMethods())
|
.flatMap(ext -> stream(ext.getClass().getMethods())
|
||||||
.filter(checkReturnType(Reply.class))
|
.filter(checkReturnType(Reply.class))
|
||||||
.map(returnReply(ext)));
|
.map(returnReply(ext)))
|
||||||
|
.flatMap(Reply::stream);
|
||||||
|
|
||||||
// Replies can be standalone or attached to abilities, fetch those too
|
// Replies can be standalone or attached to abilities, fetch those too
|
||||||
Stream<Reply> abilityReplies = abilities.values().stream()
|
Stream<Reply> abilityReplies = abilities.values().stream()
|
||||||
@ -709,7 +710,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
|||||||
/**
|
/**
|
||||||
* Invokes the method and retrieves its return {@link Reply}.
|
* Invokes the method and retrieves its return {@link Reply}.
|
||||||
*
|
*
|
||||||
* @param obj an bot or extension that this method is invoked with
|
* @param obj a bot or extension that this method is invoked with
|
||||||
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
||||||
*/
|
*/
|
||||||
private Function<? super Method, AbilityExtension> returnExtension(Object obj) {
|
private Function<? super Method, AbilityExtension> returnExtension(Object obj) {
|
||||||
@ -726,7 +727,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
|||||||
/**
|
/**
|
||||||
* Invokes the method and retrieves its return {@link Ability}.
|
* Invokes the method and retrieves its return {@link Ability}.
|
||||||
*
|
*
|
||||||
* @param obj an bot or extension that this method is invoked with
|
* @param obj a bot or extension that this method is invoked with
|
||||||
* @return a {@link Function} which returns the {@link Ability} returned by the given method
|
* @return a {@link Function} which returns the {@link Ability} returned by the given method
|
||||||
*/
|
*/
|
||||||
private Function<? super Method, Ability> returnAbility(Object obj) {
|
private Function<? super Method, Ability> returnAbility(Object obj) {
|
||||||
@ -743,7 +744,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
|||||||
/**
|
/**
|
||||||
* Invokes the method and retrieves its return {@link Reply}.
|
* Invokes the method and retrieves its return {@link Reply}.
|
||||||
*
|
*
|
||||||
* @param obj an bot or extension that this method is invoked with
|
* @param obj a bot or extension that this method is invoked with
|
||||||
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
||||||
*/
|
*/
|
||||||
private Function<? super Method, Reply> returnReply(Object obj) {
|
private Function<? super Method, Reply> returnReply(Object obj) {
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
package org.telegram.abilitybots.api.objects;
|
package org.telegram.abilitybots.api.objects;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static com.google.common.collect.Lists.newArrayList;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,18 +23,24 @@ import static java.util.Arrays.asList;
|
|||||||
*
|
*
|
||||||
* @author Abbas Abou Daya
|
* @author Abbas Abou Daya
|
||||||
*/
|
*/
|
||||||
public final class Reply {
|
public class Reply {
|
||||||
public final List<Predicate<Update>> conditions;
|
public final List<Predicate<Update>> conditions;
|
||||||
public final Consumer<Update> action;
|
public final Consumer<Update> action;
|
||||||
|
|
||||||
private Reply(List<Predicate<Update>> conditions, Consumer<Update> action) {
|
Reply(List<Predicate<Update>> conditions, Consumer<Update> action) {
|
||||||
this.conditions = conditions;
|
this.conditions = ImmutableList.<Predicate<Update>>builder()
|
||||||
|
.addAll(conditions)
|
||||||
|
.build();
|
||||||
this.action = action;
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Reply of(Consumer<Update> action, List<Predicate<Update>> conditions) {
|
||||||
|
return new Reply(conditions, action);
|
||||||
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static Reply of(Consumer<Update> action, Predicate<Update>... conditions) {
|
public static Reply of(Consumer<Update> action, Predicate<Update>... conditions) {
|
||||||
return new Reply(asList(conditions), action);
|
return Reply.of(action, newArrayList(conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOkFor(Update update) {
|
public boolean isOkFor(Update update) {
|
||||||
@ -42,6 +53,18 @@ public final class Reply {
|
|||||||
action.accept(update);
|
action.accept(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Predicate<Update>> conditions() {
|
||||||
|
return conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Consumer<Update> action() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<Reply> stream(){
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o)
|
if (this == o)
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
package org.telegram.abilitybots.api.objects;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.telegram.abilitybots.api.db.DBContext;
|
||||||
|
import org.telegram.abilitybots.api.util.AbilityUtils;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static com.google.common.collect.Lists.newArrayList;
|
||||||
|
|
||||||
|
public class ReplyFlow extends Reply {
|
||||||
|
|
||||||
|
private final Set<Reply> nextReplies;
|
||||||
|
|
||||||
|
private ReplyFlow(List<Predicate<Update>> conditions, Consumer<Update> action, Set<Reply> nextReplies) {
|
||||||
|
super(conditions, action);
|
||||||
|
this.nextReplies = nextReplies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReplyFlowBuilder builder(DBContext db) {
|
||||||
|
return new ReplyFlowBuilder(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReplyFlowBuilder builder(DBContext db, int id) {
|
||||||
|
return new ReplyFlowBuilder(db, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Reply> nextReplies() {
|
||||||
|
return nextReplies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Reply> stream() {
|
||||||
|
return Stream.concat(Stream.of(this), nextReplies.stream().flatMap(Reply::stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReplyFlowBuilder {
|
||||||
|
public static final String STATES = "user_state_replies";
|
||||||
|
private static AtomicInteger replyCounter = new AtomicInteger();
|
||||||
|
private final DBContext db;
|
||||||
|
private final int id;
|
||||||
|
private List<Predicate<Update>> conds;
|
||||||
|
private Consumer<Update> action;
|
||||||
|
private Set<Reply> nextReplies;
|
||||||
|
|
||||||
|
private ReplyFlowBuilder(DBContext db, int id) {
|
||||||
|
conds = new ArrayList<>();
|
||||||
|
nextReplies = new HashSet<>();
|
||||||
|
this.db = db;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReplyFlowBuilder(DBContext db) {
|
||||||
|
this(db, replyCounter.getAndIncrement());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlowBuilder action(Consumer<Update> action) {
|
||||||
|
this.action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlowBuilder onlyIf(Predicate<Update> pred) {
|
||||||
|
conds.add(pred);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlowBuilder next(Reply nextReply) {
|
||||||
|
List<Predicate<Update>> statefulConditions = toStateful(nextReply.conditions());
|
||||||
|
Consumer<Update> statefulAction = nextReply.action().andThen(upd -> {
|
||||||
|
Long chatId = AbilityUtils.getChatId(upd);
|
||||||
|
db.<Long, Integer>getMap(STATES).remove(chatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
Reply statefulReply = Reply.of(statefulAction, statefulConditions);
|
||||||
|
nextReplies.add(statefulReply);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlowBuilder next(ReplyFlow nextReplyFlow) {
|
||||||
|
List<Predicate<Update>> statefulConditions = toStateful(nextReplyFlow.conditions());
|
||||||
|
|
||||||
|
ReplyFlow statefulReplyFlow = new ReplyFlow(statefulConditions, nextReplyFlow.action(), nextReplyFlow.nextReplies());
|
||||||
|
nextReplies.add(statefulReplyFlow);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlow build() {
|
||||||
|
if (action == null)
|
||||||
|
action = upd -> {};
|
||||||
|
Consumer<Update> statefulAction = action.andThen(upd -> {
|
||||||
|
Long chatId = AbilityUtils.getChatId(upd);
|
||||||
|
db.<Long, Integer>getMap(STATES).put(chatId, id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ReplyFlow(conds, statefulAction, nextReplies);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private List<Predicate<Update>> toStateful(List<Predicate<Update>> conditions) {
|
||||||
|
List<Predicate<Update>> statefulConditions = newArrayList(conditions);
|
||||||
|
statefulConditions.add(0, upd -> {
|
||||||
|
Long chatId = AbilityUtils.getChatId(upd);
|
||||||
|
int stateId = db.<Long, Integer>getMap(STATES).getOrDefault(chatId, -1);
|
||||||
|
return id == stateId;
|
||||||
|
});
|
||||||
|
return statefulConditions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ import static org.apache.commons.lang3.StringUtils.EMPTY;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.internal.verification.VerificationModeFactory.times;
|
import static org.mockito.internal.verification.VerificationModeFactory.times;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.mockContext;
|
import static org.telegram.abilitybots.api.bot.TestUtils.mockContext;
|
||||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||||
|
|
||||||
class AbilityBotI18nTest {
|
class AbilityBotI18nTest {
|
||||||
|
@ -15,12 +15,7 @@ import org.telegram.abilitybots.api.sender.SilentSender;
|
|||||||
import org.telegram.abilitybots.api.util.Pair;
|
import org.telegram.abilitybots.api.util.Pair;
|
||||||
import org.telegram.abilitybots.api.util.Trio;
|
import org.telegram.abilitybots.api.util.Trio;
|
||||||
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
|
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
|
||||||
import org.telegram.telegrambots.meta.api.objects.ChatMember;
|
import org.telegram.telegrambots.meta.api.objects.*;
|
||||||
import org.telegram.telegrambots.meta.api.objects.Document;
|
|
||||||
import org.telegram.telegrambots.meta.api.objects.File;
|
|
||||||
import org.telegram.telegrambots.meta.api.objects.Message;
|
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
|
||||||
import org.telegram.telegrambots.meta.api.objects.User;
|
|
||||||
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
@ -39,15 +34,12 @@ import static java.util.Optional.empty;
|
|||||||
import static org.apache.commons.io.FileUtils.deleteQuietly;
|
import static org.apache.commons.io.FileUtils.deleteQuietly;
|
||||||
import static org.apache.commons.lang3.ArrayUtils.addAll;
|
import static org.apache.commons.lang3.ArrayUtils.addAll;
|
||||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.Mockito.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.mockito.internal.verification.VerificationModeFactory.times;
|
import static org.mockito.internal.verification.VerificationModeFactory.times;
|
||||||
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
|
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
|
||||||
|
import static org.telegram.abilitybots.api.bot.TestUtils.*;
|
||||||
|
import static org.telegram.abilitybots.api.bot.TestUtils.CREATOR;
|
||||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||||
import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT;
|
import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT;
|
||||||
import static org.telegram.abilitybots.api.objects.Flag.MESSAGE;
|
import static org.telegram.abilitybots.api.objects.Flag.MESSAGE;
|
||||||
@ -65,8 +57,6 @@ public class AbilityBotTest {
|
|||||||
private static final long GROUP_ID = 10L;
|
private static final long GROUP_ID = 10L;
|
||||||
private static final String TEST = "test";
|
private static final String TEST = "test";
|
||||||
private static final String[] TEXT = {TEST};
|
private static final String[] TEXT = {TEST};
|
||||||
public static final User USER = new User(1, "first", false, "last", "username", null);
|
|
||||||
public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null);
|
|
||||||
|
|
||||||
private DefaultBot bot;
|
private DefaultBot bot;
|
||||||
private DBContext db;
|
private DBContext db;
|
||||||
@ -93,7 +83,7 @@ public class AbilityBotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sendsPrivacyViolation() {
|
void sendsPrivacyViolation() {
|
||||||
Update update = mockFullUpdate(USER, "/admin");
|
Update update = mockFullUpdate(bot, USER, "/admin");
|
||||||
|
|
||||||
bot.onUpdateReceived(update);
|
bot.onUpdateReceived(update);
|
||||||
|
|
||||||
@ -102,7 +92,7 @@ public class AbilityBotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sendsLocalityViolation() {
|
void sendsLocalityViolation() {
|
||||||
Update update = mockFullUpdate(USER, "/group");
|
Update update = mockFullUpdate(bot, USER, "/group");
|
||||||
|
|
||||||
bot.onUpdateReceived(update);
|
bot.onUpdateReceived(update);
|
||||||
|
|
||||||
@ -112,7 +102,7 @@ public class AbilityBotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sendsInputArgsViolation() {
|
void sendsInputArgsViolation() {
|
||||||
Update update = mockFullUpdate(USER, "/count 1 2 3");
|
Update update = mockFullUpdate(bot, USER, "/count 1 2 3");
|
||||||
|
|
||||||
bot.onUpdateReceived(update);
|
bot.onUpdateReceived(update);
|
||||||
|
|
||||||
@ -121,7 +111,7 @@ public class AbilityBotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void canProcessRepliesIfSatisfyRequirements() {
|
void canProcessRepliesIfSatisfyRequirements() {
|
||||||
Update update = mockFullUpdate(USER, "must reply");
|
Update update = mockFullUpdate(bot, USER, "must reply");
|
||||||
|
|
||||||
// False means the update was not pushed down the stream since it has been consumed by the reply
|
// False means the update was not pushed down the stream since it has been consumed by the reply
|
||||||
assertFalse(bot.filterReply(update));
|
assertFalse(bot.filterReply(update));
|
||||||
@ -312,7 +302,7 @@ public class AbilityBotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void canCheckInput() {
|
void canCheckInput() {
|
||||||
Update update = mockFullUpdate(USER, "/something");
|
Update update = mockFullUpdate(bot, USER, "/something");
|
||||||
Ability abilityWithOneInput = getDefaultBuilder()
|
Ability abilityWithOneInput = getDefaultBuilder()
|
||||||
.build();
|
.build();
|
||||||
Ability abilityWithZeroInput = getDefaultBuilder()
|
Ability abilityWithZeroInput = getDefaultBuilder()
|
||||||
@ -549,25 +539,6 @@ public class AbilityBotTest {
|
|||||||
verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
|
verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
static MessageContext mockContext(User user) {
|
|
||||||
return mockContext(user, user.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
static MessageContext mockContext(User user, long groupId, String... args) {
|
|
||||||
Update update = mock(Update.class);
|
|
||||||
Message message = mock(Message.class);
|
|
||||||
|
|
||||||
when(update.hasMessage()).thenReturn(true);
|
|
||||||
when(update.getMessage()).thenReturn(message);
|
|
||||||
|
|
||||||
when(message.getFrom()).thenReturn(user);
|
|
||||||
when(message.hasText()).thenReturn(true);
|
|
||||||
|
|
||||||
return newContext(update, user, groupId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void canPrintCommandsBasedOnPrivacy() {
|
void canPrintCommandsBasedOnPrivacy() {
|
||||||
Update update = mock(Update.class);
|
Update update = mock(Update.class);
|
||||||
@ -601,27 +572,6 @@ public class AbilityBotTest {
|
|||||||
verify(silent, times(1)).send(expected, GROUP_ID);
|
verify(silent, times(1)).send(expected, GROUP_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Update mockFullUpdate(User user, String args) {
|
|
||||||
bot.users().put(USER.getId(), USER);
|
|
||||||
bot.users().put(CREATOR.getId(), CREATOR);
|
|
||||||
bot.userIds().put(CREATOR.getUserName(), CREATOR.getId());
|
|
||||||
bot.userIds().put(USER.getUserName(), USER.getId());
|
|
||||||
|
|
||||||
bot.admins().add(CREATOR.getId());
|
|
||||||
|
|
||||||
Update update = mock(Update.class);
|
|
||||||
when(update.hasMessage()).thenReturn(true);
|
|
||||||
Message message = mock(Message.class);
|
|
||||||
when(message.getFrom()).thenReturn(user);
|
|
||||||
when(message.getText()).thenReturn(args);
|
|
||||||
when(message.hasText()).thenReturn(true);
|
|
||||||
when(message.isUserMessage()).thenReturn(true);
|
|
||||||
when(message.getChatId()).thenReturn((long) user.getId());
|
|
||||||
when(update.getMessage()).thenReturn(message);
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mockUser(Update update, Message message, User user) {
|
private void mockUser(Update update, Message message, User user) {
|
||||||
when(update.hasMessage()).thenReturn(true);
|
when(update.hasMessage()).thenReturn(true);
|
||||||
when(update.getMessage()).thenReturn(message);
|
when(update.getMessage()).thenReturn(message);
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
package org.telegram.abilitybots.api.bot;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.telegram.abilitybots.api.db.DBContext;
|
||||||
|
import org.telegram.abilitybots.api.objects.Reply;
|
||||||
|
import org.telegram.abilitybots.api.objects.ReplyFlow;
|
||||||
|
import org.telegram.abilitybots.api.sender.MessageSender;
|
||||||
|
import org.telegram.abilitybots.api.sender.SilentSender;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.telegram.abilitybots.api.bot.TestUtils.USER;
|
||||||
|
import static org.telegram.abilitybots.api.bot.TestUtils.mockFullUpdate;
|
||||||
|
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||||
|
import static org.telegram.abilitybots.api.objects.ReplyFlow.ReplyFlowBuilder.STATES;
|
||||||
|
import static org.telegram.abilitybots.api.util.AbilityUtils.getChatId;
|
||||||
|
|
||||||
|
public class ReplyFlowTest {
|
||||||
|
private static final int INITIAL_STATE = 1;
|
||||||
|
private static final int INTERIM_STATE = 2;
|
||||||
|
private DBContext db;
|
||||||
|
private ReplyFlowBot bot;
|
||||||
|
|
||||||
|
private MessageSender sender;
|
||||||
|
private SilentSender silent;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
db = offlineInstance("db");
|
||||||
|
bot = new ReplyFlowBot(EMPTY, EMPTY, db);
|
||||||
|
|
||||||
|
sender = mock(MessageSender.class);
|
||||||
|
silent = mock(SilentSender.class);
|
||||||
|
|
||||||
|
bot.sender = sender;
|
||||||
|
bot.silent = silent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws IOException {
|
||||||
|
db.clear();
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotReplyIfFirstReplyFlowDoesNotMatch() {
|
||||||
|
Update update = mockFullUpdate(bot, USER, "this is not supported");
|
||||||
|
long chatId = getChatId(update);
|
||||||
|
|
||||||
|
assertTrue(bot.filterReply(update));
|
||||||
|
|
||||||
|
verify(silent, never()).send("Command me to go left or right!", chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotReplyIfLaterRepliesAreAttemptedButUserNotInRightState() {
|
||||||
|
Update update = mockFullUpdate(bot, USER, "left");
|
||||||
|
long chatId = getChatId(update);
|
||||||
|
db.<Long, Integer>getMap(STATES).put(chatId, INTERIM_STATE);
|
||||||
|
|
||||||
|
assertTrue(bot.filterReply(update));
|
||||||
|
|
||||||
|
verify(silent, never()).send("Sir, I have gone left.", chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repliesIfFirstReplyFlowMatches() {
|
||||||
|
Update update = mockFullUpdate(bot, USER, "wake up");
|
||||||
|
long chatId = getChatId(update);
|
||||||
|
|
||||||
|
assertFalse(bot.filterReply(update));
|
||||||
|
|
||||||
|
verify(silent, only()).send("Command me to go left or right!", chatId);
|
||||||
|
assertEquals(INITIAL_STATE, db.<Long, Integer>getMap(STATES).get(chatId), "User is not in the proper initial state");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stateIsNotResetOnFaultyReply() {
|
||||||
|
Update update = mockFullUpdate(bot, USER, "leffffft");
|
||||||
|
long chatId = getChatId(update);
|
||||||
|
db.<Long, Integer>getMap(STATES).put(chatId, INITIAL_STATE);
|
||||||
|
|
||||||
|
assertTrue(bot.filterReply(update));
|
||||||
|
|
||||||
|
verify(silent, never()).send("I don't know how to go left.", chatId);
|
||||||
|
assertEquals(INITIAL_STATE, db.<Long, Integer>getMap(STATES).get(chatId), "User is no longer in the initial state after faulty reply");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void terminalRepliesResetState() {
|
||||||
|
Update update = mockFullUpdate(bot, USER, "go left or else");
|
||||||
|
long chatId = getChatId(update);
|
||||||
|
db.<Long, Integer>getMap(STATES).put(chatId, INTERIM_STATE);
|
||||||
|
|
||||||
|
assertFalse(bot.filterReply(update));
|
||||||
|
|
||||||
|
verify(silent, only()).send("Sir, I have gone left.", chatId);
|
||||||
|
assertFalse(db.<Long, Integer>getMap(STATES).containsKey(chatId), "User still has state after terminal reply");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReplyFlowBot extends AbilityBot {
|
||||||
|
|
||||||
|
private ReplyFlowBot(String botToken, String botUsername, DBContext db) {
|
||||||
|
super(botToken, botUsername, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int creatorId() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyFlow directionFlow() {
|
||||||
|
Reply saidLeft = Reply.of(upd -> silent.send("Sir, I have gone left.", getChatId(upd)),
|
||||||
|
hasMessageWith("go left or else"));
|
||||||
|
|
||||||
|
ReplyFlow leftflow = ReplyFlow.builder(db, 2)
|
||||||
|
.action(upd -> silent.send("I don't know how to go left.", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("left"))
|
||||||
|
.next(saidLeft).build();
|
||||||
|
|
||||||
|
Reply saidRight = Reply.of(upd -> silent.send("Sir, I have gone right.", getChatId(upd)),
|
||||||
|
hasMessageWith("right"));
|
||||||
|
|
||||||
|
return ReplyFlow.builder(db, 1)
|
||||||
|
.action(upd -> silent.send("Command me to go left or right!", getChatId(upd)))
|
||||||
|
.onlyIf(hasMessageWith("wake up"))
|
||||||
|
.next(leftflow)
|
||||||
|
.next(saidRight)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Predicate<Update> hasMessageWith(String msg) {
|
||||||
|
return upd -> upd.getMessage().getText().equalsIgnoreCase(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package org.telegram.abilitybots.api.bot;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.telegram.abilitybots.api.objects.MessageContext;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.User;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
||||||
|
|
||||||
|
public final class TestUtils {
|
||||||
|
public static final User USER = new User(1, "first", false, "last", "username", null);
|
||||||
|
public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null);
|
||||||
|
|
||||||
|
private TestUtils() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
static Update mockFullUpdate(AbilityBot bot, User user, String args) {
|
||||||
|
bot.users().put(USER.getId(), USER);
|
||||||
|
bot.users().put(CREATOR.getId(), CREATOR);
|
||||||
|
bot.userIds().put(CREATOR.getUserName(), CREATOR.getId());
|
||||||
|
bot.userIds().put(USER.getUserName(), USER.getId());
|
||||||
|
|
||||||
|
bot.admins().add(CREATOR.getId());
|
||||||
|
|
||||||
|
Update update = mock(Update.class);
|
||||||
|
when(update.hasMessage()).thenReturn(true);
|
||||||
|
Message message = mock(Message.class);
|
||||||
|
when(message.getFrom()).thenReturn(user);
|
||||||
|
when(message.getText()).thenReturn(args);
|
||||||
|
when(message.hasText()).thenReturn(true);
|
||||||
|
when(message.isUserMessage()).thenReturn(true);
|
||||||
|
when(message.getChatId()).thenReturn((long) user.getId());
|
||||||
|
when(update.getMessage()).thenReturn(message);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
static MessageContext mockContext(User user, long groupId, String... args) {
|
||||||
|
Update update = mock(Update.class);
|
||||||
|
Message message = mock(Message.class);
|
||||||
|
|
||||||
|
when(update.hasMessage()).thenReturn(true);
|
||||||
|
when(update.getMessage()).thenReturn(message);
|
||||||
|
|
||||||
|
when(message.getFrom()).thenReturn(user);
|
||||||
|
when(message.hasText()).thenReturn(true);
|
||||||
|
|
||||||
|
return newContext(update, user, groupId, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
static MessageContext mockContext(User user) {
|
||||||
|
return mockContext(user, user.getId());
|
||||||
|
}
|
||||||
|
}
|
@ -17,8 +17,8 @@ import static java.lang.String.format;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR;
|
import static org.telegram.abilitybots.api.bot.TestUtils.CREATOR;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.USER;
|
import static org.telegram.abilitybots.api.bot.TestUtils.USER;
|
||||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||||
|
|
||||||
class MapDBContextTest {
|
class MapDBContextTest {
|
||||||
|
Loading…
Reference in New Issue
Block a user