diff --git a/TelegramBots.wiki/abilities/Advanced.md b/TelegramBots.wiki/abilities/Advanced.md index 45f90300..ec2f4617 100644 --- a/TelegramBots.wiki/abilities/Advanced.md +++ b/TelegramBots.wiki/abilities/Advanced.md @@ -33,5 +33,35 @@ As an example, if you want to restrict the updates to photos only, then you may } ``` +## Custom Command Processing +### Command Prefix +Customizing the command prefix is as simple as overriding the `getCommandPrefix` method as shown below. +```java +@Override +protected String getCommandPrefix() { + return "!"; +} +``` + +### Command Regex Split +The method that the bot uses to capture command tokens is through the regex splitters. By default, it's set to `" "`. However, this can be customized. For example, +if you'd like to split on digits and whitespaces, then you may do the following: +```java +@Override +protected String getCommandRegexSplit() { + return "\\s\\d"; +} +``` +### Commands with Continuous Text +Feeling ambitious? You may allow your bot to process tokens that are technically attached to your command. Imagine you have a command +`/do` and you'd like users to send commands as `/do1` and still trigger the `do` ability. In order to do that, override the `allowContinuousText` function. +```java +@Override +protected boolean allowContinuousText() { + return true; +} +``` +Please note that this may cause ability overlap. If multiple abilities can match the same command, the longest match will be taken. For example, +if you have two abilities `do` and `do1`, the command `/do1` will trigger the `do1` ability. ## Statistics AbilityBot can accrue basic statistics about the usage of your abilities and replies. Simply `enableStats()` on an Ability builder or `enableStats()` on replies to activate this feature. Once activated, you may call `/stats` and the bot will print a basic list of statistics. At the moment, AbilityBot only tracks hits. In the future, this will be enhanced to track more stats. \ 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 16f0a3a2..c543a435 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 @@ -260,6 +260,18 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability return true; } + protected String getCommandPrefix() { + return "/"; + } + + protected String getCommandRegexSplit() { + return " "; + } + + protected boolean allowContinuousText() { + return false; + } + /** * Registers the declared abilities using method reflection. Also, replies are accumulated using the built abilities and standalone methods that return a Reply. *

@@ -501,17 +513,27 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability if (!update.hasMessage() || !msg.hasText()) return Trio.of(update, abilities.get(DEFAULT), new String[]{}); - String[] tokens = msg.getText().split(" "); - - if (tokens[0].startsWith("/")) { - String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase(); - Ability ability = abilities.get(abilityToken); - tokens = Arrays.copyOfRange(tokens, 1, tokens.length); - return Trio.of(update, ability, tokens); + Ability ability; + String[] tokens; + if (allowContinuousText()) { + String abName = abilities.keySet().stream() + .filter(name -> msg.getText().startsWith(format("%s%s", getCommandPrefix(), name))) + .findFirst().orElse(DEFAULT); + tokens = msg.getText() + .replaceFirst(getCommandPrefix() + abName, "") + .split(getCommandRegexSplit()); + ability = abilities.get(abName); } else { - Ability ability = abilities.get(DEFAULT); - return Trio.of(update, ability, tokens); + tokens = msg.getText().split(getCommandRegexSplit()); + if (tokens[0].startsWith(getCommandPrefix())) { + String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase(); + ability = abilities.get(abilityToken); + tokens = Arrays.copyOfRange(tokens, 1, tokens.length); + } else { + ability = abilities.get(DEFAULT); + } } + return Trio.of(update, ability, tokens); } private String stripBotUsername(String token) { diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ContinuousTextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ContinuousTextTest.java new file mode 100644 index 00000000..6e204625 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ContinuousTextTest.java @@ -0,0 +1,102 @@ +package org.telegram.abilitybots.api.bot; + +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.Ability; +import org.telegram.abilitybots.api.sender.SilentSender; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.api.objects.User; + +import java.io.IOException; + +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.TestUtils.mockFullUpdate; +import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; +import static org.telegram.abilitybots.api.objects.Ability.builder; +import static org.telegram.abilitybots.api.objects.Locality.ALL; +import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; + +public class ContinuousTextTest { + private static final User USER = new User(1, "first", false, "last", "username", null); + + private DBContext db; + + private SilentSender silent; + private ContinuousTextBot bot; + + @BeforeEach + void setUp() { + db = offlineInstance("db"); + bot = new ContinuousTextBot(EMPTY, EMPTY, db); + silent = mock(SilentSender.class); + bot.silent = silent; + } + + @AfterEach + void tearDown() throws IOException { + db.clear(); + db.close(); + } + + @Test + void processesContinuousText() { + Update update = mockFullUpdate(bot, USER, "/do2"); + + bot.onUpdateReceived(update); + + verify(silent, times(1)) + .send("2", USER.getId()); + } + + @Test + void matchesLongestAbilityName() { + Update update = mockFullUpdate(bot, USER, "/do1"); + + bot.onUpdateReceived(update); + + verify(silent, times(1)) + .send("longer ability name", USER.getId()); + } + + public static class ContinuousTextBot extends AbilityBot { + + public ContinuousTextBot(String token, String username, DBContext db) { + super(token, username, db); + } + + @Override + public int creatorId() { + return 1337; + } + + @Override + protected boolean allowContinuousText() { + return true; + } + + public Ability continuousTextAbility() { + return builder() + .name("do") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> silent.send(ctx.firstArg(), ctx.chatId())) + .build(); + } + + public Ability continuousTextSimilarAbility() { + return builder() + .name("do1") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> silent.send("longer ability name", ctx.chatId())) + .build(); + } + } +} \ No newline at end of file