From 727f5a7a3ed302439f9d3b359bfd601613a45cd6 Mon Sep 17 00:00:00 2001 From: Bernhard Kralofsky Date: Tue, 9 Jul 2019 21:39:02 +0200 Subject: [PATCH 1/9] prevent nullPointerExceptions when using message flags without a MESSAGE flag first --- .../org/telegram/abilitybots/api/objects/Flag.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java index f59cc25c..0bf5a905 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java @@ -27,12 +27,12 @@ public enum Flag implements Predicate { CHOSEN_INLINE_QUERY(Update::hasChosenInlineQuery), // Message Flags - REPLY(update -> update.getMessage().isReply()), - DOCUMENT(upd -> upd.getMessage().hasDocument()), - TEXT(upd -> upd.getMessage().hasText()), - PHOTO(upd -> upd.getMessage().hasPhoto()), - LOCATION(upd -> upd.getMessage().hasLocation()), - CAPTION(upd -> nonNull(upd.getMessage().getCaption())); + REPLY(upd -> upd.hasMessage() && upd.getMessage().isReply()), + DOCUMENT(upd -> upd.hasMessage() && upd.getMessage().hasDocument()), + TEXT(upd -> upd.hasMessage() && upd.getMessage().hasText()), + PHOTO(upd -> upd.hasMessage() && upd.getMessage().hasPhoto()), + LOCATION(upd -> upd.hasMessage() && upd.getMessage().hasLocation()), + CAPTION(upd -> upd.hasMessage() && nonNull(upd.getMessage().getCaption())); private final Predicate predicate; From afe35b4b142689bde2eaeaeb42786e92768e72cd Mon Sep 17 00:00:00 2001 From: nagorny Date: Sat, 24 Aug 2019 18:57:34 +0300 Subject: [PATCH 2/9] Fixed #652 --- .../java/org/telegram/telegrambots/bots/DefaultAbsSender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java index 0372eeb9..b94d3a09 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java @@ -414,7 +414,7 @@ public abstract class DefaultAbsSender extends AbsSender { return sendAudio.deserializeResponse(sendHttpPostRequest(httppost)); } catch (IOException e) { - throw new TelegramApiException("Unable to send sticker", e); + throw new TelegramApiException("Unable to send audio", e); } } From aa3448544e142594a77d2cda8c8756283f3dcff3 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 16 Sep 2019 22:37:16 -0700 Subject: [PATCH 3/9] Support backup and recovery of db vars --- .../abilitybots/api/db/BackupVar.java | 45 +++++++++++++++++++ .../abilitybots/api/db/MapDBContext.java | 15 +++---- .../abilitybots/api/db/MapDBContextTest.java | 19 ++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/BackupVar.java diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/BackupVar.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/BackupVar.java new file mode 100644 index 00000000..d2b6d03c --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/BackupVar.java @@ -0,0 +1,45 @@ +package org.telegram.abilitybots.api.db; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Objects; + +public class BackupVar { + @JsonProperty("var") + private final T var; + + private BackupVar(T var) { + this.var = var; + } + + @JsonCreator + public static BackupVar createVar(@JsonProperty("var") R var) { + return new BackupVar<>(var); + } + + public T var() { + return var; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BackupVar backupVar = (BackupVar) o; + return Objects.equals(var, backupVar.var); + } + + @Override + public int hashCode() { + return Objects.hash(var); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("var", var) + .toString(); + } +} diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java index a6977917..ab0fa07f 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java @@ -12,12 +12,7 @@ import org.mapdb.Serializer; import org.telegram.abilitybots.api.util.Pair; import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; +import java.util.*; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; @@ -188,15 +183,15 @@ public class MapDBContext implements DBContext { return Pair.of(entry.getKey(), newArrayList((List) struct)); else if (struct instanceof Map) return Pair.of(entry.getKey(), new BackupMap((Map) struct)); - else - return Pair.of(entry.getKey(), struct); + else if (struct instanceof Atomic.Var) + return Pair.of(entry.getKey(), BackupVar.createVar(((Atomic.Var) struct).get())); + return Pair.of(entry.getKey(), struct); }).collect(toMap(pair -> (String) pair.a(), Pair::b)); } private void doRecover(Map backupData) { clear(); backupData.forEach((name, value) -> { - if (value instanceof Set) { Set entrySet = (Set) value; getSet(name).addAll(entrySet); @@ -206,6 +201,8 @@ public class MapDBContext implements DBContext { } else if (value instanceof List) { List entryList = (List) value; getList(name).addAll(entryList); + } else if (value instanceof BackupVar) { + getVar(name).set(((BackupVar) value).var()); } else { log.error(format("Unable to identify object type during DB recovery, entry name: %s", name)); } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java index 267450fd..f67dc6ca 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java @@ -12,6 +12,7 @@ import java.util.Set; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; +import static com.google.common.collect.Sets.toImmutableEnumSet; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -38,6 +39,24 @@ class MapDBContextTest { db.close(); } + @Test + void canRecoverVar() { + Var test = db.getVar(TEST); + String val = "abilitybot"; + test.set(val); + + Object backup = db.backup(); + db.clear(); + // db.clear does not clear atomic variables + // TODO: get clear to remove all non-collection variables in DB + test.set("somevalue"); + boolean recovered = db.recover(backup); + String recoveredVal = db.getVar(TEST).get(); + + assertTrue(recovered, "Could not recover JSON backup file"); + assertEquals(val, recoveredVal, "Could not properly recover val from Var in DB"); + } + @Test void canRecoverDB() { Map users = db.getMap(USERS); From b3c6623eaa8e4bc96a0802af4e2075b4783049c8 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 16 Sep 2019 22:44:33 -0700 Subject: [PATCH 4/9] Change sender to silent for the usage of forceReply in wiki --- TelegramBots.wiki/abilities/Using-Replies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TelegramBots.wiki/abilities/Using-Replies.md b/TelegramBots.wiki/abilities/Using-Replies.md index 82307360..5ca5f1b8 100644 --- a/TelegramBots.wiki/abilities/Using-Replies.md +++ b/TelegramBots.wiki/abilities/Using-Replies.md @@ -32,7 +32,7 @@ public Ability playWithMe() { .privacy(PUBLIC) .locality(ALL) .input(0) - .action(ctx -> sender.forceReply(playMessage, ctx.chatId())) + .action(ctx -> silent.forceReply(playMessage, ctx.chatId())) // The signature of a reply is -> (Consumer action, Predicate... conditions) // So, we first declare the action that takes an update (NOT A MESSAGECONTEXT) like the action above // The reason of that is that a reply can be so versatile depending on the message, context becomes an inefficient wrapping From 7b7478f180eccad0b361dfb81221aec60fc0474b Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 16 Sep 2019 22:48:22 -0700 Subject: [PATCH 5/9] Another sender to silent change --- TelegramBots.wiki/abilities/Using-Replies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TelegramBots.wiki/abilities/Using-Replies.md b/TelegramBots.wiki/abilities/Using-Replies.md index 5ca5f1b8..0067a67b 100644 --- a/TelegramBots.wiki/abilities/Using-Replies.md +++ b/TelegramBots.wiki/abilities/Using-Replies.md @@ -40,7 +40,7 @@ public Ability playWithMe() { // Prints to console System.out.println("I'm in a reply!"); // Sends message - sender.send("It's been nice playing with you!", upd.getMessage().getChatId()); + silent.send("It's been nice playing with you!", upd.getMessage().getChatId()); }, // Now we start declaring conditions, MESSAGE is a member of the enum Flag class // That class contains out-of-the-box predicates for your replies! From ed7333a21e9c62e6a93dd644b9cdf9e62adef224 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 16 Sep 2019 23:40:20 -0700 Subject: [PATCH 6/9] Fix executeAsync in SilentSender to properly call sender.executeAsync --- .../abilitybots/api/sender/SilentSender.java | 7 ++-- .../api/sender/SilentSenderTest.java | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java index b5d48bfb..bc87c328 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java @@ -7,6 +7,7 @@ import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.replykeyboard.ForceReplyKeyboard; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import org.telegram.telegrambots.meta.updateshandlers.SentCallback; import java.io.Serializable; import java.util.Optional; @@ -52,12 +53,12 @@ public class SilentSender { } } - public > Optional executeAsync(Method method) { + public , Callback extends SentCallback> void + executeAsync(Method method, Callback callable) { try { - return Optional.ofNullable(sender.execute(method)); + sender.executeAsync(method, callable); } catch (TelegramApiException e) { log.error("Could not execute bot API method", e); - return Optional.empty(); } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java index 96da662c..884cf72d 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java @@ -2,15 +2,19 @@ package org.telegram.abilitybots.api.sender; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.telegram.telegrambots.meta.api.methods.BotApiMethod; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException; +import org.telegram.telegrambots.meta.updateshandlers.SentCallback; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class SilentSenderTest { private SilentSender silent; @@ -40,4 +44,34 @@ class SilentSenderTest { assertEquals(data, execute.get(), "Silent execution resulted in a different object"); } + + @Test + void callsAsyncVariantOfExecute() throws TelegramApiException { + SendMessage methodObject = new SendMessage(); + NoOpCallback callback = new NoOpCallback(); + + silent.executeAsync(methodObject, callback); + + verify(sender, only()).executeAsync(methodObject, callback); + } + + private class NoOpCallback implements SentCallback { + + @Override + public void onResult(BotApiMethod method, Message response) { + + } + + @Override + public void onError(BotApiMethod method, TelegramApiRequestException apiException) { + + } + + @Override + public void onException(BotApiMethod method, Exception exception) { + + } + } + + ; } \ No newline at end of file From 837b4d236073715dd2fef85017ca718abdb4ec4d Mon Sep 17 00:00:00 2001 From: Bernhard Date: Tue, 17 Sep 2019 10:51:14 +0300 Subject: [PATCH 7/9] requested syntax changes --- .../org/telegram/abilitybots/api/objects/Flag.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java index 0bf5a905..3712dc9a 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java @@ -27,12 +27,12 @@ public enum Flag implements Predicate { CHOSEN_INLINE_QUERY(Update::hasChosenInlineQuery), // Message Flags - REPLY(upd -> upd.hasMessage() && upd.getMessage().isReply()), - DOCUMENT(upd -> upd.hasMessage() && upd.getMessage().hasDocument()), - TEXT(upd -> upd.hasMessage() && upd.getMessage().hasText()), - PHOTO(upd -> upd.hasMessage() && upd.getMessage().hasPhoto()), - LOCATION(upd -> upd.hasMessage() && upd.getMessage().hasLocation()), - CAPTION(upd -> upd.hasMessage() && nonNull(upd.getMessage().getCaption())); + REPLY(upd -> MESSAGE.test(upd) && upd.getMessage().isReply()), + DOCUMENT(upd -> MESSAGE.test(upd) && upd.getMessage().hasDocument()), + TEXT(upd -> MESSAGE.test(upd) && upd.getMessage().hasText()), + PHOTO(upd -> MESSAGE.test(upd) && upd.getMessage().hasPhoto()), + LOCATION(upd -> MESSAGE.test(upd) && upd.getMessage().hasLocation()), + CAPTION(upd -> MESSAGE.test(upd) && nonNull(upd.getMessage().getCaption())); private final Predicate predicate; From 0ff63149f7e61ed8bf553a1726a2e031e5371b29 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Wed, 25 Sep 2019 23:25:11 -0700 Subject: [PATCH 8/9] Add ReplyFlow implementation, tests, and wiki This commit also has a some minor test refactoring. --- TelegramBots.wiki/_Sidebar.md | 1 + TelegramBots.wiki/abilities/State-Machines.md | 134 ++++++++++++++++ .../abilities/img/replyflow_diagram.svg | 3 + .../abilitybots/api/bot/BaseAbilityBot.java | 9 +- .../abilitybots/api/objects/Reply.java | 31 +++- .../abilitybots/api/objects/ReplyFlow.java | 117 ++++++++++++++ .../api/bot/AbilityBotI18nTest.java | 2 +- .../abilitybots/api/bot/AbilityBotTest.java | 70 ++------- .../abilitybots/api/bot/ReplyFlowTest.java | 145 ++++++++++++++++++ .../abilitybots/api/bot/TestUtils.java | 60 ++++++++ .../abilitybots/api/db/MapDBContextTest.java | 4 +- 11 files changed, 505 insertions(+), 71 deletions(-) create mode 100644 TelegramBots.wiki/abilities/State-Machines.md create mode 100644 TelegramBots.wiki/abilities/img/replyflow_diagram.svg create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/ReplyFlow.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ReplyFlowTest.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/TestUtils.java diff --git a/TelegramBots.wiki/_Sidebar.md b/TelegramBots.wiki/_Sidebar.md index f423f02e..7e68bf7c 100644 --- a/TelegramBots.wiki/_Sidebar.md +++ b/TelegramBots.wiki/_Sidebar.md @@ -8,6 +8,7 @@ * [[Simple Example]] * [[Hello Ability]] * [[Using Replies]] + * [[State Machines]] * [[Database Handling]] * [[Bot Testing]] * [[Bot Recovery]] diff --git a/TelegramBots.wiki/abilities/State-Machines.md b/TelegramBots.wiki/abilities/State-Machines.md new file mode 100644 index 00000000..92d2af42 --- /dev/null +++ b/TelegramBots.wiki/abilities/State-Machines.md @@ -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 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: +
+ +![Alt text](./img/replyflow_diagram.svg) + + +
+ +## 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 ; + } + + 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 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, ) +``` \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/img/replyflow_diagram.svg b/TelegramBots.wiki/abilities/img/replyflow_diagram.svg new file mode 100644 index 00000000..b5311ead --- /dev/null +++ b/TelegramBots.wiki/abilities/img/replyflow_diagram.svg @@ -0,0 +1,3 @@ + + +
wake up
wake up
Command me to go left or right!
Command me to go left or right!
right
right
Sir, I have gone right. 
Sir, I have gone right. 
left
left
I don't know how to go left.
I don't know how to go left.
go left or else
go left or else
Sir, I have gone left.
Sir, I have gone left.
user
user
bot
bot
\ No newline at end of file diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java index d5f9c0ef..7a6143ab 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java @@ -680,7 +680,8 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability Stream extensionReplies = extensions.stream() .flatMap(ext -> stream(ext.getClass().getMethods()) .filter(checkReturnType(Reply.class)) - .map(returnReply(ext))); + .map(returnReply(ext))) + .flatMap(Reply::stream); // Replies can be standalone or attached to abilities, fetch those too Stream 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}. * - * @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 */ private Function returnExtension(Object obj) { @@ -726,7 +727,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements 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 */ private Function returnAbility(Object obj) { @@ -743,7 +744,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability /** * 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 */ private Function returnReply(Object obj) { diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java index a74e4e2f..6af4c921 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java @@ -1,14 +1,19 @@ package org.telegram.abilitybots.api.objects; 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 java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; +import static com.google.common.collect.Lists.newArrayList; import static java.util.Arrays.asList; /** @@ -18,18 +23,24 @@ import static java.util.Arrays.asList; * * @author Abbas Abou Daya */ -public final class Reply { +public class Reply { public final List> conditions; public final Consumer action; - private Reply(List> conditions, Consumer action) { - this.conditions = conditions; + Reply(List> conditions, Consumer action) { + this.conditions = ImmutableList.>builder() + .addAll(conditions) + .build(); this.action = action; } + public static Reply of(Consumer action, List> conditions) { + return new Reply(conditions, action); + } + @SafeVarargs public static Reply of(Consumer action, Predicate... conditions) { - return new Reply(asList(conditions), action); + return Reply.of(action, newArrayList(conditions)); } public boolean isOkFor(Update update) { @@ -42,6 +53,18 @@ public final class Reply { action.accept(update); } + public List> conditions() { + return conditions; + } + + public Consumer action() { + return action; + } + + public Stream stream(){ + return Stream.of(this); + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/ReplyFlow.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/ReplyFlow.java new file mode 100644 index 00000000..799a1a9b --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/ReplyFlow.java @@ -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 nextReplies; + + private ReplyFlow(List> conditions, Consumer action, Set 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 nextReplies() { + return nextReplies; + } + + @Override + public Stream 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> conds; + private Consumer action; + private Set 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 action) { + this.action = action; + return this; + } + + public ReplyFlowBuilder onlyIf(Predicate pred) { + conds.add(pred); + return this; + } + + public ReplyFlowBuilder next(Reply nextReply) { + List> statefulConditions = toStateful(nextReply.conditions()); + Consumer statefulAction = nextReply.action().andThen(upd -> { + Long chatId = AbilityUtils.getChatId(upd); + db.getMap(STATES).remove(chatId); + }); + + Reply statefulReply = Reply.of(statefulAction, statefulConditions); + nextReplies.add(statefulReply); + return this; + } + + public ReplyFlowBuilder next(ReplyFlow nextReplyFlow) { + List> 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 statefulAction = action.andThen(upd -> { + Long chatId = AbilityUtils.getChatId(upd); + db.getMap(STATES).put(chatId, id); + }); + + return new ReplyFlow(conds, statefulAction, nextReplies); + } + + @NotNull + private List> toStateful(List> conditions) { + List> statefulConditions = newArrayList(conditions); + statefulConditions.add(0, upd -> { + Long chatId = AbilityUtils.getChatId(upd); + int stateId = db.getMap(STATES).getOrDefault(chatId, -1); + return id == stateId; + }); + return statefulConditions; + } + } +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 5494c595..e92cb442 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -15,7 +15,7 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; 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; class AbilityBotI18nTest { diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index a1c5e8a5..1fe24f5c 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -15,12 +15,7 @@ import org.telegram.abilitybots.api.sender.SilentSender; import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Trio; import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; -import org.telegram.telegrambots.meta.api.objects.ChatMember; -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.api.objects.*; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; 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.lang3.ArrayUtils.addAll; import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -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.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; 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.objects.Flag.DOCUMENT; 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 String TEST = "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 DBContext db; @@ -93,7 +83,7 @@ public class AbilityBotTest { @Test void sendsPrivacyViolation() { - Update update = mockFullUpdate(USER, "/admin"); + Update update = mockFullUpdate(bot, USER, "/admin"); bot.onUpdateReceived(update); @@ -102,7 +92,7 @@ public class AbilityBotTest { @Test void sendsLocalityViolation() { - Update update = mockFullUpdate(USER, "/group"); + Update update = mockFullUpdate(bot, USER, "/group"); bot.onUpdateReceived(update); @@ -112,7 +102,7 @@ public class AbilityBotTest { @Test void sendsInputArgsViolation() { - Update update = mockFullUpdate(USER, "/count 1 2 3"); + Update update = mockFullUpdate(bot, USER, "/count 1 2 3"); bot.onUpdateReceived(update); @@ -121,7 +111,7 @@ public class AbilityBotTest { @Test 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 assertFalse(bot.filterReply(update)); @@ -312,7 +302,7 @@ public class AbilityBotTest { @Test void canCheckInput() { - Update update = mockFullUpdate(USER, "/something"); + Update update = mockFullUpdate(bot, USER, "/something"); Ability abilityWithOneInput = getDefaultBuilder() .build(); Ability abilityWithZeroInput = getDefaultBuilder() @@ -549,25 +539,6 @@ public class AbilityBotTest { 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 void canPrintCommandsBasedOnPrivacy() { Update update = mock(Update.class); @@ -601,27 +572,6 @@ public class AbilityBotTest { 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) { when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ReplyFlowTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ReplyFlowTest.java new file mode 100644 index 00000000..fb198278 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ReplyFlowTest.java @@ -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.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.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.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.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.getMap(STATES).put(chatId, INTERIM_STATE); + + assertFalse(bot.filterReply(update)); + + verify(silent, only()).send("Sir, I have gone left.", chatId); + assertFalse(db.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 hasMessageWith(String msg) { + return upd -> upd.getMessage().getText().equalsIgnoreCase(msg); + } + } +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/TestUtils.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/TestUtils.java new file mode 100644 index 00000000..19c531da --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/TestUtils.java @@ -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()); + } +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java index 267450fd..594a38a8 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java @@ -16,8 +16,8 @@ import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR; -import static org.telegram.abilitybots.api.bot.AbilityBotTest.USER; +import static org.telegram.abilitybots.api.bot.TestUtils.CREATOR; +import static org.telegram.abilitybots.api.bot.TestUtils.USER; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; class MapDBContextTest { From 5d60c72a46e8593aa075cb1b8764bab0c65c73fd Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Sun, 29 Sep 2019 23:14:23 -0700 Subject: [PATCH 9/9] Add wiki for Ability Extensions --- TelegramBots.wiki/_Sidebar.md | 1 + .../abilities/Ability-Extensions.md | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 TelegramBots.wiki/abilities/Ability-Extensions.md diff --git a/TelegramBots.wiki/_Sidebar.md b/TelegramBots.wiki/_Sidebar.md index f423f02e..85647408 100644 --- a/TelegramBots.wiki/_Sidebar.md +++ b/TelegramBots.wiki/_Sidebar.md @@ -9,6 +9,7 @@ * [[Hello Ability]] * [[Using Replies]] * [[Database Handling]] + * [[Ability Extensions]] * [[Bot Testing]] * [[Bot Recovery]] * [[Advanced]] diff --git a/TelegramBots.wiki/abilities/Ability-Extensions.md b/TelegramBots.wiki/abilities/Ability-Extensions.md new file mode 100644 index 00000000..60f5a482 --- /dev/null +++ b/TelegramBots.wiki/abilities/Ability-Extensions.md @@ -0,0 +1,40 @@ +# Ability Extensions +You have around 100 abilities in your bot and you're looking for a way to refactor that mess into more modular classes. `AbillityExtension` is here to support just that! It's not a secret that AbilityBot uses refactoring backstage to be able to construct all of your abilities and map them accordingly. However, AbilityBot searches initially for all methods that return an `AbilityExtension` type. Then, those extensions will be used to search for declared abilities. Here's an example. +```java +public class MrGoodGuy implements AbilityExtension { + public Ability () { + return Ability.builder() + .name("nice") + .privacy(PUBLIC) + .locality(ALL) + .action(ctx -> silent.send("You're awesome!", ctx.chatId()) + ); + } +} + +public class MrBadGuy implements AbilityExtension { + public Ability () { + return Ability.builder() + .name("notnice") + .privacy(PUBLIC) + .locality(ALL) + .action(ctx -> silent.send("You're horrible!", ctx.chatId()) + ); + } + } + + public class YourAwesomeBot implements AbilityBot { + + // Constructor for your bot + + public AbilityExtension goodGuy() { + return new MrGoodGuy(); + } + + public AbilityExtension badGuy() { + return new MrBadGuy(); + } + + // Override creatorId + } +``` \ No newline at end of file