diff --git a/TelegramBots.wiki/_Sidebar.md b/TelegramBots.wiki/_Sidebar.md
index 5e54f1b3..6368e265 100644
--- a/TelegramBots.wiki/_Sidebar.md
+++ b/TelegramBots.wiki/_Sidebar.md
@@ -8,6 +8,7 @@
* [[Simple Example]]
* [[Hello Ability]]
* [[Using Replies]]
+ * [[Ability Toggle]]
* [[State Machines]]
* [[Database Handling]]
* [[Ability Extensions]]
diff --git a/TelegramBots.wiki/abilities/Ability-Toggle.md b/TelegramBots.wiki/abilities/Ability-Toggle.md
new file mode 100644
index 00000000..0c15864c
--- /dev/null
+++ b/TelegramBots.wiki/abilities/Ability-Toggle.md
@@ -0,0 +1,42 @@
+# Ability Toggle
+Well, what if you don't like all the default abilities that AbilityBot supplies? Fear not, you are able to disable all of them, even rename them if you will!
+
+You may pass a custom toggle to your abilitybot constructor to customize how these abilities get registered.
+
+## The Barebone Toggle
+The barebone toggle is used to **turn off** all the default abilities that come with the bot (ban, unban, demote, promte, etc...). To use the barebone toggle, call your super constructor with:
+```java
+import org.telegram.abilitybots.api.toggle.BareboneToggle;
+
+public class YourAwesomeBot extends AbilityBot {
+ public YourAwesomeBot(String token, String username) {
+ BareboneToggle toggle = new BareboneToggle();
+ super(token, username, toggle);
+ }
+
+ // Override ceatorId()
+}
+```
+Obviously, you can export this as a parameter that you can pass to your bot's constructor.
+
+## The Custom Toggle
+The custom toggle allows you to customize or turn off the names of the abilities that the abilitybot exposes.
+```java
+import org.telegram.abilitybots.api.toggle.CustomToggle;
+
+public class YourAwesomeBot extends AbilityBot {
+ public YourAwesomeBot(String token, String username) {
+ CustomToggle toggle = new CustomToggle()
+ .turnOff("ban")
+ .toggle("promote", "upgrade");
+
+ super(token, username, toggle);
+ }
+
+ // Override ceatorId()
+}
+```
+
+With these changes, the ability "ban" is no longer available and the "promote" ability has been renamed to "upgrade".
+## The Default Toggle
+The default toggle is automatically used if the user does not specify a toggle. The default toggle allows all the abilities to be effective and unchanged.
\ No newline at end of file
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java
index de3016e1..12894316 100644
--- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java
@@ -1,6 +1,8 @@
package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext;
+import org.telegram.abilitybots.api.toggle.AbilityToggle;
+import org.telegram.abilitybots.api.toggle.DefaultToggle;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
@@ -16,18 +18,34 @@ import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
* @author Abbas Abou Daya
*/
public abstract class AbilityBot extends BaseAbilityBot implements LongPollingBot {
- protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
- super(botToken, botUsername, db, botOptions);
+ protected AbilityBot(String botToken, String botUsername, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
+ super(botToken, botUsername, db, toggle, botOptions);
}
- protected AbilityBot(String botToken, String botUsername, DBContext db) {
- this(botToken, botUsername, db, new DefaultBotOptions());
+ protected AbilityBot(String botToken, String botUsername, AbilityToggle toggle, DefaultBotOptions options) {
+ this(botToken, botUsername, onlineInstance(botUsername), toggle, options);
+ }
+
+ protected AbilityBot(String botToken, String botUsername, DBContext db, AbilityToggle toggle) {
+ this(botToken, botUsername, db, toggle, new DefaultBotOptions());
+ }
+
+ protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions options) {
+ this(botToken, botUsername, db, new DefaultToggle(), options);
}
protected AbilityBot(String botToken, String botUsername, DefaultBotOptions botOptions) {
this(botToken, botUsername, onlineInstance(botUsername), botOptions);
}
+ protected AbilityBot(String botToken, String botUsername, AbilityToggle toggle) {
+ this(botToken, botUsername, onlineInstance(botUsername), toggle);
+ }
+
+ protected AbilityBot(String botToken, String botUsername, DBContext db) {
+ this(botToken, botUsername, db, new DefaultToggle());
+ }
+
protected AbilityBot(String botToken, String botUsername) {
this(botToken, botUsername, onlineInstance(botUsername));
}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityWebhookBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityWebhookBot.java
index 04887d7c..2c9b3db6 100644
--- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityWebhookBot.java
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityWebhookBot.java
@@ -1,6 +1,8 @@
package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext;
+import org.telegram.abilitybots.api.toggle.AbilityToggle;
+import org.telegram.abilitybots.api.toggle.DefaultToggle;
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.bots.DefaultBotOptions;
@@ -21,19 +23,35 @@ public abstract class AbilityWebhookBot extends BaseAbilityBot implements Webhoo
private final String botPath;
- protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, DefaultBotOptions botOptions) {
- super(botToken, botUsername, db, botOptions);
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
+ super(botToken, botUsername, db, toggle, botOptions);
this.botPath = botPath;
}
- protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db) {
- this(botToken, botUsername, botPath, db, new DefaultBotOptions());
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, AbilityToggle toggle, DefaultBotOptions options) {
+ this(botToken, botUsername, botPath, onlineInstance(botUsername), toggle, options);
+ }
+
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, AbilityToggle toggle) {
+ this(botToken, botUsername, botPath, db, toggle, new DefaultBotOptions());
+ }
+
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, DefaultBotOptions options) {
+ this(botToken, botUsername, botPath, db, new DefaultToggle(), options);
}
protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DefaultBotOptions botOptions) {
this(botToken, botUsername, botPath, onlineInstance(botUsername), botOptions);
}
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, AbilityToggle toggle) {
+ this(botToken, botUsername, botPath, onlineInstance(botUsername), toggle);
+ }
+
+ protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db) {
+ this(botToken, botUsername, botPath, db, new DefaultToggle());
+ }
+
protected AbilityWebhookBot(String botToken, String botUsername, String botPath) {
this(botToken, botUsername, botPath, onlineInstance(botUsername));
}
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 7a6143ab..ae1ffda2 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
@@ -3,21 +3,15 @@ package org.telegram.abilitybots.api.bot;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimap;
-import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.telegram.abilitybots.api.db.DBContext;
-import org.telegram.abilitybots.api.objects.Ability;
-import org.telegram.abilitybots.api.objects.Locality;
-import org.telegram.abilitybots.api.objects.MessageContext;
-import org.telegram.abilitybots.api.objects.Privacy;
-import org.telegram.abilitybots.api.objects.Reply;
+import org.telegram.abilitybots.api.objects.*;
import org.telegram.abilitybots.api.sender.DefaultSender;
import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.sender.SilentSender;
+import org.telegram.abilitybots.api.toggle.AbilityToggle;
import org.telegram.abilitybots.api.util.AbilityExtension;
import org.telegram.abilitybots.api.util.AbilityUtils;
import org.telegram.abilitybots.api.util.Pair;
@@ -25,85 +19,31 @@ import org.telegram.abilitybots.api.util.Trio;
import org.telegram.telegrambots.bots.DefaultAbsSender;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
-import org.telegram.telegrambots.meta.api.methods.GetFile;
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
-import org.telegram.telegrambots.meta.api.methods.send.SendDocument;
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 java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static com.google.common.base.Strings.isNullOrEmpty;
-import static com.google.common.collect.MultimapBuilder.hashKeys;
import static java.lang.String.format;
import static java.time.ZonedDateTime.now;
import static java.util.Arrays.stream;
-import static java.util.Comparator.comparing;
-import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.compile;
-import static java.util.stream.Collectors.joining;
-import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.telegram.abilitybots.api.objects.Ability.builder;
-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.REPLY;
-import static org.telegram.abilitybots.api.objects.Locality.ALL;
-import static org.telegram.abilitybots.api.objects.Locality.GROUP;
-import static org.telegram.abilitybots.api.objects.Locality.USER;
+import static org.telegram.abilitybots.api.objects.Locality.*;
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
-import static org.telegram.abilitybots.api.objects.Privacy.ADMIN;
-import static org.telegram.abilitybots.api.objects.Privacy.CREATOR;
-import static org.telegram.abilitybots.api.objects.Privacy.GROUP_ADMIN;
-import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_BAN_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_BAN_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_CLAIM_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_CLAIM_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_COMMANDS_NOT_FOUND;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_DEMOTE_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_DEMOTE_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_PROMOTE_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_PROMOTE_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_RECOVER_ERROR;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_RECOVER_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_RECOVER_MESSAGE;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_RECOVER_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_UNBAN_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_UNBAN_SUCCESS;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.CHECK_INPUT_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.CHECK_LOCALITY_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.CHECK_PRIVACY_FAIL;
-import static org.telegram.abilitybots.api.util.AbilityMessageCodes.USER_NOT_FOUND;
-import static org.telegram.abilitybots.api.util.AbilityUtils.addTag;
-import static org.telegram.abilitybots.api.util.AbilityUtils.commitTo;
-import static org.telegram.abilitybots.api.util.AbilityUtils.getChatId;
-import static org.telegram.abilitybots.api.util.AbilityUtils.getLocalizedMessage;
-import static org.telegram.abilitybots.api.util.AbilityUtils.isGroupUpdate;
-import static org.telegram.abilitybots.api.util.AbilityUtils.isSuperGroupUpdate;
-import static org.telegram.abilitybots.api.util.AbilityUtils.isUserMessage;
-import static org.telegram.abilitybots.api.util.AbilityUtils.shortName;
-import static org.telegram.abilitybots.api.util.AbilityUtils.stripTag;
+import static org.telegram.abilitybots.api.objects.Privacy.*;
+import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
+import static org.telegram.abilitybots.api.util.AbilityUtils.*;
/**
* The father of all ability bots. Bots that need to utilize abilities need to extend this bot.
@@ -141,29 +81,21 @@ import static org.telegram.abilitybots.api.util.AbilityUtils.stripTag;
public abstract class BaseAbilityBot extends DefaultAbsSender implements AbilityExtension {
private static final Logger log = LogManager.getLogger(BaseAbilityBot.class);
+ protected static final String DEFAULT = "default";
// DB objects
public static final String ADMINS = "ADMINS";
public static final String USERS = "USERS";
public static final String USER_ID = "USER_ID";
public static final String BLACKLIST = "BLACKLIST";
- // Factory commands
- protected static final String DEFAULT = "default";
- protected static final String CLAIM = "claim";
- protected static final String BAN = "ban";
- protected static final String PROMOTE = "promote";
- protected static final String DEMOTE = "demote";
- protected static final String UNBAN = "unban";
- protected static final String BACKUP = "backup";
- protected static final String RECOVER = "recover";
- protected static final String COMMANDS = "commands";
- protected static final String REPORT = "report";
-
// DB and sender
protected final DBContext db;
protected MessageSender sender;
protected SilentSender silent;
+ // Ability toggle
+ private final AbilityToggle toggle;
+
// Bot token and username
private final String botToken;
private final String botUsername;
@@ -176,12 +108,13 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
public abstract int creatorId();
- protected BaseAbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
+ protected BaseAbilityBot(String botToken, String botUsername, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
super(botOptions);
this.botToken = botToken;
this.botUsername = botUsername;
this.db = db;
+ this.toggle = toggle;
this.sender = new DefaultSender(this);
silent = new SilentSender(sender);
@@ -281,374 +214,6 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return true;
}
- /**
- * Gets the user with the specified username.
- *
- * @param username the username of the required user
- * @return the user
- */
- protected User getUser(String username) {
- Integer id = userIds().get(username.toLowerCase());
- if (id == null) {
- throw new IllegalStateException(format("Could not find ID corresponding to username [%s]", username));
- }
-
- return getUser(id);
- }
-
- /**
- * Gets the user with the specified ID.
- *
- * @param id the id of the required user
- * @return the user
- */
- protected User getUser(int id) {
- User user = users().get(id);
- if (user == null) {
- throw new IllegalStateException(format("Could not find user corresponding to id [%d]", id));
- }
-
- return user;
- }
-
- /**
- * Gets the user with the specified username. If user was not found, the bot will send a message on Telegram.
- *
- * @param username the username of the required user
- * @param ctx the message context with the originating user
- * @return the id of the user
- */
- protected int getUserIdSendError(String username, MessageContext ctx) {
- try {
- return getUser(username).getId();
- } catch (IllegalStateException ex) {
- silent.send(getLocalizedMessage(USER_NOT_FOUND, ctx.user().getLanguageCode(), username), ctx.chatId());
- throw ex;
- }
- }
-
- /**
- *
- * Format of the report:
- *
- * [command1] - [description1]
- *
- * [command2] - [description2]
- *
- * ...
- *
- * Once you invoke it, the bot will send the available commands to the chat. This is a public ability so anyone can invoke it.
- *
- * Usage: /commands
- *
- * @return the ability to report commands defined by the child bot.
- */
- public Ability reportCommands() {
- return builder()
- .name(REPORT)
- .locality(ALL)
- .privacy(CREATOR)
- .input(0)
- .action(ctx -> {
- String commands = abilities.values().stream()
- .filter(ability -> nonNull(ability.info()))
- .map(ability -> {
- String name = ability.name();
- String info = ability.info();
- return format("%s - %s", name, info);
- })
- .sorted()
- .reduce((a, b) -> format("%s%n%s", a, b))
- .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode()));
-
- silent.send(commands, ctx.chatId());
- })
- .build();
- }
-
- /**
- * Default format:
- *
- * PUBLIC
- *
- * [command1] - [description1]
- *
- * [command2] - [description2]
- *
- * GROUP_ADMIN
- *
- * [command1] - [description1]
- *
- * ...
- *
- * @return the ability to print commands based on the privacy of the requesting user
- */
- public Ability commands() {
- return builder()
- .name(COMMANDS)
- .locality(USER)
- .privacy(PUBLIC)
- .input(0)
- .action(ctx -> {
- Privacy privacy = getPrivacy(ctx.update(), ctx.user().getId());
-
- ListMultimap abilitiesPerPrivacy = abilities.values().stream()
- .map(ability -> {
- String name = ability.name();
- String info = ability.info();
-
- if (!isEmpty(info))
- return Pair.of(ability.privacy(), format("/%s - %s", name, info));
- return Pair.of(ability.privacy(), format("/%s", name));
- })
- .sorted(comparing(Pair::b))
- .collect(() -> hashKeys().arrayListValues().build(),
- (map, pair) -> map.put(pair.a(), pair.b()),
- Multimap::putAll);
-
- String commands = abilitiesPerPrivacy.asMap().entrySet().stream()
- .filter(entry -> privacy.compareTo(entry.getKey()) >= 0)
- .sorted(comparing(Entry::getKey))
- .map(entry ->
- entry.getValue().stream()
- .reduce(entry.getKey().toString(), (a, b) -> format("%s\n%s", a, b))
- )
- .collect(joining("\n"));
-
- if (commands.isEmpty())
- commands = getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode());
-
- silent.send(commands, ctx.chatId());
- })
- .build();
- }
-
- /**
- * This backup ability returns the object defined by {@link DBContext#backup()} as a message document.
- *
- * This is a high-profile ability and is restricted to the CREATOR only.
- *
- * Usage: /backup
- *
- * @return the ability to back-up the database of the bot
- */
- public Ability backupDB() {
- return builder()
- .name(BACKUP)
- .locality(USER)
- .privacy(CREATOR)
- .input(0)
- .action(ctx -> {
- File backup = new File("backup.json");
-
- try (PrintStream printStream = new PrintStream(backup)) {
- printStream.print(db.backup());
- sender.sendDocument(new SendDocument()
- .setDocument(backup)
- .setChatId(ctx.chatId())
- );
- } catch (FileNotFoundException e) {
- log.error("Error while fetching backup", e);
- } catch (TelegramApiException e) {
- log.error("Error while sending document/backup file", e);
- }
- })
- .build();
- }
-
- /**
- * Recovers the bot database using {@link DBContext#recover(Object)}.
- *
- * The bot recovery process hugely depends on the implementation of the recovery method of {@link DBContext}.
- *
- * Usage: /recover
- *
- * @return the ability to recover the database of the bot
- */
- public Ability recoverDB() {
- return builder()
- .name(RECOVER)
- .locality(USER)
- .privacy(CREATOR)
- .input(0)
- .action(ctx -> silent.forceReply(
- getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().getLanguageCode()), ctx.chatId()))
- .reply(update -> {
- String replyToMsg = update.getMessage().getReplyToMessage().getText();
- String recoverMessage = getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(update).getLanguageCode());
- if (!replyToMsg.equals(recoverMessage))
- return;
-
- String fileId = update.getMessage().getDocument().getFileId();
- try (FileReader reader = new FileReader(downloadFileWithId(fileId))) {
- String backupData = IOUtils.toString(reader);
- if (db.recover(backupData)) {
- send(ABILITY_RECOVER_SUCCESS, update);
- } else {
- send(ABILITY_RECOVER_FAIL, update);
- }
- } catch (Exception e) {
- log.error("Could not recover DB from backup", e);
- send(ABILITY_RECOVER_ERROR, update);
- }
- }, MESSAGE, DOCUMENT, REPLY)
- .build();
- }
-
- /**
- * Banned users are accumulated in the blacklist. Use {@link DBContext#getSet(String)} with name specified by {@link BaseAbilityBot#BLACKLIST}.
- *
- * Usage: /ban @username
- *
- * Note that admins who try to ban the creator, get banned.
- *
- * @return the ability to ban the user from any kind of bot interaction
- */
- public Ability banUser() {
- return builder()
- .name(BAN)
- .locality(ALL)
- .privacy(ADMIN)
- .input(1)
- .action(ctx -> {
- String username = stripTag(ctx.firstArg());
- int userId = getUserIdSendError(username, ctx);
- String bannedUser;
-
- // Protection from abuse
- if (userId == creatorId()) {
- userId = ctx.user().getId();
- bannedUser = isNullOrEmpty(ctx.user().getUserName()) ? addTag(ctx.user().getUserName()) : shortName(ctx.user());
- } else {
- bannedUser = addTag(username);
- }
-
- Set blacklist = blacklist();
- if (blacklist.contains(userId))
- sendMd(ABILITY_BAN_FAIL, ctx, escape(bannedUser));
- else {
- blacklist.add(userId);
- sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser));
- }
- })
- .post(commitTo(db))
- .build();
- }
-
- /**
- * Usage: /unban @username
- *
- * @return the ability to unban a user
- */
- public Ability unbanUser() {
- return builder()
- .name(UNBAN)
- .locality(ALL)
- .privacy(ADMIN)
- .input(1)
- .action(ctx -> {
- String username = stripTag(ctx.firstArg());
- Integer userId = getUserIdSendError(username, ctx);
-
- Set blacklist = blacklist();
-
- if (!blacklist.remove(userId))
- silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
- else {
- silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
- }
- })
- .post(commitTo(db))
- .build();
- }
-
- /**
- * @return the ability to promote a user to a bot admin
- */
- public Ability promoteAdmin() {
- return builder()
- .name(PROMOTE)
- .locality(ALL)
- .privacy(ADMIN)
- .input(1)
- .action(ctx -> {
- String username = stripTag(ctx.firstArg());
- Integer userId = getUserIdSendError(username, ctx);
-
- Set admins = admins();
- if (admins.contains(userId))
- sendMd(ABILITY_PROMOTE_FAIL, ctx, escape(username));
- else {
- admins.add(userId);
- sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username));
- }
- }).post(commitTo(db))
- .build();
- }
-
- /**
- * @return the ability to demote an admin to a user
- */
- public Ability demoteAdmin() {
- return builder()
- .name(DEMOTE)
- .locality(ALL)
- .privacy(ADMIN)
- .input(1)
- .action(ctx -> {
- String username = stripTag(ctx.firstArg());
- Integer userId = getUserIdSendError(username, ctx);
-
- Set admins = admins();
- if (admins.remove(userId)) {
- sendMd(ABILITY_DEMOTE_SUCCESS, ctx, escape(username));
- } else {
- sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username));
- }
- })
- .post(commitTo(db))
- .build();
- }
-
- /**
- * Regular users and admins who try to claim the bot will get banned.
- *
- * @return the ability to claim yourself as the master and creator of the bot
- */
- public Ability claimCreator() {
- return builder()
- .name(CLAIM)
- .locality(ALL)
- .privacy(CREATOR)
- .input(0)
- .action(ctx -> {
- Set admins = admins();
- int id = creatorId();
-
- if (admins.contains(id))
- send(ABILITY_CLAIM_FAIL, ctx);
- else {
- admins.add(id);
- send(ABILITY_CLAIM_SUCCESS, ctx);
- }
- })
- .post(commitTo(db))
- .build();
- }
-
- private Optional send(String message, MessageContext ctx, String... args) {
- return silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
- }
-
- private Optional sendMd(String message, MessageContext ctx, String... args) {
- return silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
- }
-
- private Optional send(String message, Update upd) {
- Long chatId = upd.getMessage().getChatId();
- return silent.send(getLocalizedMessage(message, AbilityUtils.getUser(upd).getLanguageCode()), chatId);
- }
-
/**
* Registers the declared abilities using method reflection. Also, replies are accumulated using the built abilities and standalone methods that return a Reply.
*
@@ -665,11 +230,19 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
// Add the bot itself as it is an AbilityExtension
extensions.add(this);
+ DefaultAbilities defaultAbs = new DefaultAbilities(this);
+ Stream defaultAbsStream = stream(DefaultAbilities.class.getMethods())
+ .filter(checkReturnType(Ability.class))
+ .map(returnAbility(defaultAbs))
+ .filter(ab -> !toggle.isOff(ab))
+ .map(toggle::processAbility);
+
// Extract all abilities from every single extension instance
- abilities = extensions.stream()
+ abilities = Stream.concat(defaultAbsStream,
+ extensions.stream()
.flatMap(ext -> stream(ext.getClass().getMethods())
.filter(checkReturnType(Ability.class))
- .map(returnAbility(ext)))
+ .map(returnAbility(ext))))
// Abilities are immutable, build it respectively
.collect(ImmutableMap::builder,
(b, a) -> b.put(a.name(), a),
@@ -703,7 +276,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
* @param clazz the type to be tested
* @return a predicate testing the return type of the method corresponding to the class parameter
*/
- private Predicate checkReturnType(Class> clazz) {
+ private static Predicate checkReturnType(Class> clazz) {
return method -> clazz.isAssignableFrom(method.getReturnType());
}
@@ -730,7 +303,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
* @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 super Method, Ability> returnAbility(Object obj) {
+ private static Function super Method, Ability> returnAbility(Object obj) {
return method -> {
try {
return (Ability) method.invoke(obj);
@@ -747,7 +320,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
* @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 super Method, Reply> returnReply(Object obj) {
+ private static Function super Method, Reply> returnReply(Object obj) {
return method -> {
try {
return (Reply) method.invoke(obj);
@@ -834,7 +407,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
}
@NotNull
- private Privacy getPrivacy(Update update, int id) {
+ Privacy getPrivacy(Update update, int id) {
return isCreator(id) ?
CREATOR : isAdmin(id) ?
ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ?
@@ -939,13 +512,4 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return ability.flags().stream()
.reduce(true, flagAnd, Boolean::logicalAnd);
}
-
- private File downloadFileWithId(String fileId) throws TelegramApiException {
- return sender.downloadFile(sender.execute(new GetFile().setFileId(fileId)));
- }
-
-
- private String escape(String username) {
- return username.replace("_", "\\_");
- }
}
\ No newline at end of file
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/DefaultAbilities.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/DefaultAbilities.java
new file mode 100644
index 00000000..3d79a5c1
--- /dev/null
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/DefaultAbilities.java
@@ -0,0 +1,435 @@
+package org.telegram.abilitybots.api.bot;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.commons.io.IOUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.telegram.abilitybots.api.db.DBContext;
+import org.telegram.abilitybots.api.objects.Ability;
+import org.telegram.abilitybots.api.objects.MessageContext;
+import org.telegram.abilitybots.api.objects.Privacy;
+import org.telegram.abilitybots.api.util.AbilityExtension;
+import org.telegram.abilitybots.api.util.AbilityUtils;
+import org.telegram.abilitybots.api.util.Pair;
+import org.telegram.telegrambots.meta.api.methods.GetFile;
+import org.telegram.telegrambots.meta.api.methods.send.SendDocument;
+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 java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.MultimapBuilder.hashKeys;
+import static java.lang.String.format;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.nonNull;
+import static java.util.stream.Collectors.joining;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.telegram.abilitybots.api.objects.Ability.builder;
+import static org.telegram.abilitybots.api.objects.Flag.*;
+import static org.telegram.abilitybots.api.objects.Locality.ALL;
+import static org.telegram.abilitybots.api.objects.Locality.USER;
+import static org.telegram.abilitybots.api.objects.Privacy.*;
+import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
+import static org.telegram.abilitybots.api.util.AbilityUtils.*;
+
+public final class DefaultAbilities implements AbilityExtension {
+ // Default commands
+ public static final String CLAIM = "claim";
+ public static final String BAN = "ban";
+ public static final String PROMOTE = "promote";
+ public static final String DEMOTE = "demote";
+ public static final String UNBAN = "unban";
+ public static final String BACKUP = "backup";
+ public static final String RECOVER = "recover";
+ public static final String COMMANDS = "commands";
+ public static final String REPORT = "report";
+ private static final Logger log = LogManager.getLogger(DefaultAbilities.class);
+ private final BaseAbilityBot bot;
+
+ public DefaultAbilities(BaseAbilityBot bot) {
+ this.bot = bot;
+ }
+
+ /**
+ *
+ * Format of the report:
+ *
+ * [command1] - [description1]
+ *
+ * [command2] - [description2]
+ *
+ * ...
+ *
+ * Once you invoke it, the bot will send the available commands to the chat. This is a public ability so anyone can invoke it.
+ *
+ * Usage: /commands
+ *
+ * @return the ability to report commands defined by the child bot.
+ */
+ public Ability reportCommands() {
+ return builder()
+ .name(REPORT)
+ .locality(ALL)
+ .privacy(CREATOR)
+ .input(0)
+ .action(ctx -> {
+ String commands = bot.abilities().values().stream()
+ .filter(ability -> nonNull(ability.info()))
+ .map(ability -> {
+ String name = ability.name();
+ String info = ability.info();
+ return format("%s - %s", name, info);
+ })
+ .sorted()
+ .reduce((a, b) -> format("%s%n%s", a, b))
+ .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode()));
+
+ bot.silent.send(commands, ctx.chatId());
+ })
+ .build();
+ }
+
+ /**
+ * Default format:
+ *
+ * PUBLIC
+ *
+ * [command1] - [description1]
+ *
+ * [command2] - [description2]
+ *
+ * GROUP_ADMIN
+ *
+ * [command1] - [description1]
+ *
+ * ...
+ *
+ * @return the ability to print commands based on the privacy of the requesting user
+ */
+ public Ability commands() {
+ return builder()
+ .name(COMMANDS)
+ .locality(USER)
+ .privacy(PUBLIC)
+ .input(0)
+ .action(ctx -> {
+ Privacy privacy = bot.getPrivacy(ctx.update(), ctx.user().getId());
+
+ ListMultimap abilitiesPerPrivacy = bot.abilities().values().stream()
+ .map(ability -> {
+ String name = ability.name();
+ String info = ability.info();
+
+ if (!isEmpty(info))
+ return Pair.of(ability.privacy(), format("/%s - %s", name, info));
+ return Pair.of(ability.privacy(), format("/%s", name));
+ })
+ .sorted(comparing(Pair::b))
+ .collect(() -> hashKeys().arrayListValues().build(),
+ (map, pair) -> map.put(pair.a(), pair.b()),
+ Multimap::putAll);
+
+ String commands = abilitiesPerPrivacy.asMap().entrySet().stream()
+ .filter(entry -> privacy.compareTo(entry.getKey()) >= 0)
+ .sorted(comparing(Map.Entry::getKey))
+ .map(entry ->
+ entry.getValue().stream()
+ .reduce(entry.getKey().toString(), (a, b) -> format("%s\n%s", a, b))
+ )
+ .collect(joining("\n"));
+
+ if (commands.isEmpty())
+ commands = getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode());
+
+ bot.silent.send(commands, ctx.chatId());
+ })
+ .build();
+ }
+
+ /**
+ * This backup ability returns the object defined by {@link DBContext#backup()} as a message document.
+ *
+ * This is a high-profile ability and is restricted to the CREATOR only.
+ *
+ * Usage: /backup
+ *
+ * @return the ability to back-up the database of the bot
+ */
+ public Ability backupDB() {
+ return builder()
+ .name(BACKUP)
+ .locality(USER)
+ .privacy(CREATOR)
+ .input(0)
+ .action(ctx -> {
+ File backup = new File("backup.json");
+
+ try (PrintStream printStream = new PrintStream(backup)) {
+ printStream.print(bot.db.backup());
+ bot.sender.sendDocument(new SendDocument()
+ .setDocument(backup)
+ .setChatId(ctx.chatId())
+ );
+ } catch (FileNotFoundException e) {
+ log.error("Error while fetching backup", e);
+ } catch (TelegramApiException e) {
+ log.error("Error while sending document/backup file", e);
+ }
+ })
+ .build();
+ }
+
+ /**
+ * Recovers the bot database using {@link DBContext#recover(Object)}.
+ *
+ * The bot recovery process hugely depends on the implementation of the recovery method of {@link DBContext}.
+ *
+ * Usage: /recover
+ *
+ * @return the ability to recover the database of the bot
+ */
+ public Ability recoverDB() {
+ return builder()
+ .name(RECOVER)
+ .locality(USER)
+ .privacy(CREATOR)
+ .input(0)
+ .action(ctx -> bot.silent.forceReply(
+ getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().getLanguageCode()), ctx.chatId()))
+ .reply(update -> {
+ String replyToMsg = update.getMessage().getReplyToMessage().getText();
+ String recoverMessage = getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(update).getLanguageCode());
+ if (!replyToMsg.equals(recoverMessage))
+ return;
+
+ String fileId = update.getMessage().getDocument().getFileId();
+ try (FileReader reader = new FileReader(downloadFileWithId(fileId))) {
+ String backupData = IOUtils.toString(reader);
+ if (bot.db.recover(backupData)) {
+ send(ABILITY_RECOVER_SUCCESS, update);
+ } else {
+ send(ABILITY_RECOVER_FAIL, update);
+ }
+ } catch (Exception e) {
+ log.error("Could not recover DB from backup", e);
+ send(ABILITY_RECOVER_ERROR, update);
+ }
+ }, MESSAGE, DOCUMENT, REPLY)
+ .build();
+ }
+
+ /**
+ * Banned users are accumulated in the blacklist. Use {@link DBContext#getSet(String)} with name specified by {@link BaseAbilityBot#BLACKLIST}.
+ *
+ * Usage: /ban @username
+ *
+ * Note that admins who try to ban the creator, get banned.
+ *
+ * @return the ability to ban the user from any kind of bot interaction
+ */
+ public Ability banUser() {
+ return builder()
+ .name(BAN)
+ .locality(ALL)
+ .privacy(ADMIN)
+ .input(1)
+ .action(ctx -> {
+ String username = stripTag(ctx.firstArg());
+ int userId = getUserIdSendError(username, ctx);
+ String bannedUser;
+
+ // Protection from abuse
+ if (userId == bot.creatorId()) {
+ userId = ctx.user().getId();
+ bannedUser = isNullOrEmpty(ctx.user().getUserName()) ? addTag(ctx.user().getUserName()) : shortName(ctx.user());
+ } else {
+ bannedUser = addTag(username);
+ }
+
+ Set blacklist = bot.blacklist();
+ if (blacklist.contains(userId))
+ sendMd(ABILITY_BAN_FAIL, ctx, escape(bannedUser));
+ else {
+ blacklist.add(userId);
+ sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser));
+ }
+ })
+ .post(commitTo(bot.db))
+ .build();
+ }
+
+ /**
+ * Usage: /unban @username
+ *
+ * @return the ability to unban a user
+ */
+ public Ability unbanUser() {
+ return builder()
+ .name(UNBAN)
+ .locality(ALL)
+ .privacy(ADMIN)
+ .input(1)
+ .action(ctx -> {
+ String username = stripTag(ctx.firstArg());
+ Integer userId = getUserIdSendError(username, ctx);
+
+ Set blacklist = bot.blacklist();
+
+ if (!blacklist.remove(userId))
+ bot.silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
+ else {
+ bot.silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
+ }
+ })
+ .post(commitTo(bot.db))
+ .build();
+ }
+
+ /**
+ * @return the ability to promote a user to a bot admin
+ */
+ public Ability promoteAdmin() {
+ return builder()
+ .name(PROMOTE)
+ .locality(ALL)
+ .privacy(ADMIN)
+ .input(1)
+ .action(ctx -> {
+ String username = stripTag(ctx.firstArg());
+ Integer userId = getUserIdSendError(username, ctx);
+
+ Set admins = bot.admins();
+ if (admins.contains(userId))
+ sendMd(ABILITY_PROMOTE_FAIL, ctx, escape(username));
+ else {
+ admins.add(userId);
+ sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username));
+ }
+ }).post(commitTo(bot.db))
+ .build();
+ }
+
+ /**
+ * @return the ability to demote an admin to a user
+ */
+ public Ability demoteAdmin() {
+ return builder()
+ .name(DEMOTE)
+ .locality(ALL)
+ .privacy(ADMIN)
+ .input(1)
+ .action(ctx -> {
+ String username = stripTag(ctx.firstArg());
+ Integer userId = getUserIdSendError(username, ctx);
+
+ Set admins = bot.admins();
+ if (admins.remove(userId)) {
+ sendMd(ABILITY_DEMOTE_SUCCESS, ctx, escape(username));
+ } else {
+ sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username));
+ }
+ })
+ .post(commitTo(bot.db))
+ .build();
+ }
+
+ /**
+ * Regular users and admins who try to claim the bot will get banned.
+ *
+ * @return the ability to claim yourself as the master and creator of the bot
+ */
+ public Ability claimCreator() {
+ return builder()
+ .name(CLAIM)
+ .locality(ALL)
+ .privacy(CREATOR)
+ .input(0)
+ .action(ctx -> {
+ Set admins = bot.admins();
+ int id = bot.creatorId();
+
+ if (admins.contains(id))
+ send(ABILITY_CLAIM_FAIL, ctx);
+ else {
+ admins.add(id);
+ send(ABILITY_CLAIM_SUCCESS, ctx);
+ }
+ })
+ .post(commitTo(bot.db))
+ .build();
+ }
+
+ /**
+ * Gets the user with the specified username.
+ *
+ * @param username the username of the required user
+ * @return the user
+ */
+ private User getUser(String username) {
+ Integer id = bot.userIds().get(username.toLowerCase());
+ if (id == null) {
+ throw new IllegalStateException(format("Could not find ID corresponding to username [%s]", username));
+ }
+
+ return getUser(id);
+ }
+
+ /**
+ * Gets the user with the specified ID.
+ *
+ * @param id the id of the required user
+ * @return the user
+ */
+ private User getUser(int id) {
+ User user = bot.users().get(id);
+ if (user == null) {
+ throw new IllegalStateException(format("Could not find user corresponding to id [%d]", id));
+ }
+
+ return user;
+ }
+
+ /**
+ * Gets the user with the specified username. If user was not found, the bot will send a message on Telegram.
+ *
+ * @param username the username of the required user
+ * @param ctx the message context with the originating user
+ * @return the id of the user
+ */
+ private int getUserIdSendError(String username, MessageContext ctx) {
+ try {
+ return getUser(username).getId();
+ } catch (IllegalStateException ex) {
+ bot.silent.send(getLocalizedMessage(USER_NOT_FOUND, ctx.user().getLanguageCode(), username), ctx.chatId());
+ throw ex;
+ }
+ }
+
+
+ private Optional send(String message, MessageContext ctx, String... args) {
+ return bot.silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
+ }
+
+ private Optional sendMd(String message, MessageContext ctx, String... args) {
+ return bot.silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
+ }
+
+ private Optional send(String message, Update upd) {
+ Long chatId = upd.getMessage().getChatId();
+ return bot.silent.send(getLocalizedMessage(message, AbilityUtils.getUser(upd).getLanguageCode()), chatId);
+ }
+
+ protected File downloadFileWithId(String fileId) throws TelegramApiException {
+ return bot.sender.downloadFile(bot.sender.execute(new GetFile().setFileId(fileId)));
+ }
+}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Ability.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Ability.java
index c3004c8b..db2db4c3 100644
--- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Ability.java
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Ability.java
@@ -147,8 +147,8 @@ public final class Ability {
private Privacy privacy;
private Locality locality;
private int argNum;
- private Consumer consumer;
- private Consumer postConsumer;
+ private Consumer action;
+ private Consumer postAction;
private List replies;
private Predicate[] flags;
@@ -157,7 +157,7 @@ public final class Ability {
}
public AbilityBuilder action(Consumer consumer) {
- this.consumer = consumer;
+ this.action = consumer;
return this;
}
@@ -191,8 +191,8 @@ public final class Ability {
return this;
}
- public AbilityBuilder post(Consumer postConsumer) {
- this.postConsumer = postConsumer;
+ public AbilityBuilder post(Consumer postAction) {
+ this.postAction = postAction;
return this;
}
@@ -202,8 +202,21 @@ public final class Ability {
return this;
}
+ public AbilityBuilder basedOn(Ability ability) {
+ replies.clear();
+ replies.addAll(ability.replies());
+
+ return name(ability.name())
+ .info(ability.info())
+ .input(ability.tokens())
+ .locality(ability.locality())
+ .privacy(ability.privacy())
+ .action(ability.action())
+ .post(ability.postAction());
+ }
+
public Ability build() {
- return new Ability(name, info, locality, privacy, argNum, consumer, postConsumer, replies, flags);
+ return new Ability(name, info, locality, privacy, argNum, action, postAction, replies, flags);
}
}
}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/AbilityToggle.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/AbilityToggle.java
new file mode 100644
index 00000000..01986b43
--- /dev/null
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/AbilityToggle.java
@@ -0,0 +1,21 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.telegram.abilitybots.api.objects.Ability;
+
+/**
+ * This interface can be used to toggle or customize unwanted default abilities by the user.
+ */
+public interface AbilityToggle {
+ /**
+ * @param ab the target ability
+ * @return true if the ability has been turned off
+ */
+ boolean isOff(Ability ab);
+
+ /**
+ * Abilities that are ON (and have failed the {@link AbilityToggle#isOff} condition) will be processed by this method.
+ * @param ab the target ability
+ * @return the processed ability
+ */
+ Ability processAbility(Ability ab);
+}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/BareboneToggle.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/BareboneToggle.java
new file mode 100644
index 00000000..3e791923
--- /dev/null
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/BareboneToggle.java
@@ -0,0 +1,20 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.telegram.abilitybots.api.objects.Ability;
+
+/**
+ * This toggle can be used as-is to turn off ALL the default abilities supplied by the library.
+ * This is for users who are interested in the barebone functionality of AbilityBot.
+ */
+public class BareboneToggle implements AbilityToggle {
+ @Override
+ public boolean isOff(Ability ability) {
+ return true;
+ }
+
+ @Override
+ public Ability processAbility(Ability ab) {
+ // Should never hit this
+ throw new RuntimeException("Should not process any ability in a vanilla toggle");
+ }
+}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/CustomToggle.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/CustomToggle.java
new file mode 100644
index 00000000..22e02fa8
--- /dev/null
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/CustomToggle.java
@@ -0,0 +1,56 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.telegram.abilitybots.api.objects.Ability;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This custom toggle can be used to customize default abilities supplied by the library. Users can call {@link CustomToggle#toggle} to
+ * rename the default abilites or {@link CustomToggle#turnOff} to simply turn off the said ability.
+ */
+public class CustomToggle implements AbilityToggle {
+ public static final String OFF = "turn_off_base_ability";
+
+ private final Map baseMapping;
+
+ public CustomToggle() {
+ baseMapping = new HashMap<>();
+ }
+
+ @Override
+ public boolean isOff(Ability ability) {
+ return OFF.equalsIgnoreCase(baseMapping.get(ability.name()));
+ }
+
+ @Override
+ public Ability processAbility(Ability ability) {
+ if (baseMapping.containsKey(ability.name())) {
+ return Ability.builder()
+ .basedOn(ability)
+ .name(baseMapping.get(ability.name()))
+ .build();
+ }
+
+ return ability;
+ }
+
+ /**
+ * @param abilityName the ability you want to change
+ * @param targetName the final name for this ability
+ * @return the toggle instance
+ */
+ public CustomToggle toggle(String abilityName, String targetName) {
+ baseMapping.put(abilityName, targetName);
+ return this;
+ }
+
+ /**
+ * @param ability the ability name you would like turned off
+ * @return the toggle instance
+ */
+ public CustomToggle turnOff(String ability) {
+ baseMapping.put(ability, OFF);
+ return this;
+ }
+}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/DefaultToggle.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/DefaultToggle.java
new file mode 100644
index 00000000..3b9219ff
--- /dev/null
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/DefaultToggle.java
@@ -0,0 +1,19 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.telegram.abilitybots.api.objects.Ability;
+
+/**
+ * If the user does not supply a toggle to their constructor, the default toggle will be instantiated.
+ * This default toggle allows all the default abilities to be registered.
+ */
+public class DefaultToggle implements AbilityToggle {
+ @Override
+ public boolean isOff(Ability ability) {
+ return false;
+ }
+
+ @Override
+ public Ability processAbility(Ability ab) {
+ return ab;
+ }
+}
diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java
index 749fd4a7..ec213667 100644
--- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java
+++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java
@@ -244,4 +244,8 @@ public final class AbilityUtils {
return name.toString();
}
+
+ public static String escape(String username) {
+ return username.replace("_", "\\_");
+ }
}
\ No newline at end of file
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 e92cb442..6915d30d 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
@@ -24,6 +24,7 @@ class AbilityBotI18nTest {
private DBContext db;
private NoPublicCommandsBot bot;
+ private DefaultAbilities defaultAbs;
private MessageSender sender;
private SilentSender silent;
@@ -32,6 +33,7 @@ class AbilityBotI18nTest {
void setUp() {
db = offlineInstance("db");
bot = new NoPublicCommandsBot(EMPTY, EMPTY, db);
+ defaultAbs = new DefaultAbilities(bot);
sender = mock(MessageSender.class);
silent = mock(SilentSender.class);
@@ -50,7 +52,7 @@ class AbilityBotI18nTest {
void missingPublicCommandsLocalizedInEnglishByDefault() {
MessageContext context = mockContext(NO_LANGUAGE_USER);
- bot.reportCommands().action().accept(context);
+ defaultAbs.reportCommands().action().accept(context);
verify(silent, times(1))
.send("No available commands found.", NO_LANGUAGE_USER.getId());
@@ -60,7 +62,7 @@ class AbilityBotI18nTest {
void missingPublicCommandsLocalizedInItalian() {
MessageContext context = mockContext(ITALIAN_USER);
- bot.reportCommands().action().accept(context);
+ defaultAbs.reportCommands().action().accept(context);
verify(silent, times(1))
.send("Non sono presenti comandi disponibile.", ITALIAN_USER.getId());
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 1fe24f5c..36717ac0 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
@@ -59,6 +59,7 @@ public class AbilityBotTest {
private static final String[] TEXT = {TEST};
private DefaultBot bot;
+ private DefaultAbilities defaultAbs;
private DBContext db;
private MessageSender sender;
private SilentSender silent;
@@ -67,6 +68,7 @@ public class AbilityBotTest {
void setUp() {
db = offlineInstance("db");
bot = new DefaultBot(EMPTY, EMPTY, db);
+ defaultAbs = new DefaultAbilities(bot);
sender = mock(MessageSender.class);
silent = mock(SilentSender.class);
@@ -122,7 +124,7 @@ public class AbilityBotTest {
void canBackupDB() throws TelegramApiException {
MessageContext context = defaultContext();
- bot.backupDB().action().accept(context);
+ defaultAbs.backupDB().action().accept(context);
deleteQuietly(new java.io.File("backup.json"));
verify(sender, times(1)).sendDocument(any());
@@ -137,7 +139,7 @@ public class AbilityBotTest {
// Support for null parameter matching since due to mocking API changes
when(sender.downloadFile(ArgumentMatchers.isNull())).thenReturn(backupFile);
- bot.recoverDB().replies().get(0).actOn(update);
+ defaultAbs.recoverDB().replies().get(0).actOn(update);
verify(silent, times(1)).send(RECOVER_SUCCESS, GROUP_ID);
assertEquals(db.getSet(TEST), newHashSet(TEST), "Bot recovered but the DB is still not in sync");
@@ -159,7 +161,7 @@ public class AbilityBotTest {
MessageContext context = defaultContext();
- bot.demoteAdmin().action().accept(context);
+ defaultAbs.demoteAdmin().action().accept(context);
Set actual = bot.admins();
Set expected = emptySet();
@@ -172,7 +174,7 @@ public class AbilityBotTest {
MessageContext context = defaultContext();
- bot.promoteAdmin().action().accept(context);
+ defaultAbs.promoteAdmin().action().accept(context);
Set actual = bot.admins();
Set expected = newHashSet(USER.getId());
@@ -184,7 +186,7 @@ public class AbilityBotTest {
addUsers(USER);
MessageContext context = defaultContext();
- bot.banUser().action().accept(context);
+ defaultAbs.banUser().action().accept(context);
Set actual = bot.blacklist();
Set expected = newHashSet(USER.getId());
@@ -198,7 +200,7 @@ public class AbilityBotTest {
MessageContext context = defaultContext();
- bot.unbanUser().action().accept(context);
+ defaultAbs.unbanUser().action().accept(context);
Set actual = bot.blacklist();
Set expected = newHashSet();
@@ -215,7 +217,7 @@ public class AbilityBotTest {
addUsers(USER, CREATOR);
MessageContext context = mockContext(USER, GROUP_ID, CREATOR.getUserName());
- bot.banUser().action().accept(context);
+ defaultAbs.banUser().action().accept(context);
Set actual = bot.blacklist();
Set expected = newHashSet(USER.getId());
@@ -233,7 +235,7 @@ public class AbilityBotTest {
void creatorCanClaimBot() {
MessageContext context = mockContext(CREATOR, GROUP_ID);
- bot.claimCreator().action().accept(context);
+ defaultAbs.claimCreator().action().accept(context);
Set actual = bot.admins();
Set expected = newHashSet(CREATOR.getId());
@@ -534,7 +536,7 @@ public class AbilityBotTest {
void canReportCommands() {
MessageContext context = mockContext(USER, GROUP_ID);
- bot.reportCommands().action().accept(context);
+ defaultAbs.reportCommands().action().accept(context);
verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
}
@@ -549,7 +551,7 @@ public class AbilityBotTest {
when(message.hasText()).thenReturn(true);
MessageContext creatorCtx = newContext(update, CREATOR, GROUP_ID);
- bot.commands().action().accept(creatorCtx);
+ defaultAbs.commands().action().accept(creatorCtx);
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report";
verify(silent, times(1)).send(expected, GROUP_ID);
@@ -566,7 +568,7 @@ public class AbilityBotTest {
MessageContext userCtx = newContext(update, USER, GROUP_ID);
- bot.commands().action().accept(userCtx);
+ defaultAbs.commands().action().accept(userCtx);
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test";
verify(silent, times(1)).send(expected, GROUP_ID);
diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java
index c6f843d7..12db53f1 100644
--- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java
+++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java
@@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.Ability;
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder;
+import org.telegram.abilitybots.api.toggle.AbilityToggle;
import static org.telegram.abilitybots.api.objects.Ability.builder;
import static org.telegram.abilitybots.api.objects.Flag.CALLBACK_QUERY;
@@ -17,6 +18,10 @@ public class DefaultBot extends AbilityBot {
super(token, username, db);
}
+ public DefaultBot(String token, String username, DBContext db, AbilityToggle toggle) {
+ super(token, username, db, toggle);
+ }
+
public static AbilityBuilder getDefaultBuilder() {
return builder()
.name("test")
diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/BareboneToggleTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/BareboneToggleTest.java
new file mode 100644
index 00000000..9adca9d4
--- /dev/null
+++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/BareboneToggleTest.java
@@ -0,0 +1,47 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.telegram.abilitybots.api.bot.DefaultAbilities;
+import org.telegram.abilitybots.api.bot.DefaultBot;
+import org.telegram.abilitybots.api.db.DBContext;
+
+import java.io.IOException;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
+
+public class BareboneToggleTest {
+
+ private DBContext db;
+ private AbilityToggle toggle;
+ private DefaultBot bareboneBot;
+ private DefaultAbilities defaultAbs;
+
+ @BeforeEach
+ void setUp() {
+ db = offlineInstance("db");
+ toggle = new BareboneToggle();
+ bareboneBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+ defaultAbs = new DefaultAbilities(bareboneBot);
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ db.clear();
+ db.close();
+ }
+
+ @Test
+ public void turnsOffAllAbilities() {
+ assertFalse(bareboneBot.abilities().containsKey(DefaultAbilities.CLAIM));
+ }
+
+ @Test
+ public void throwsOnProcessingAbility() {
+ Assertions.assertThrows(RuntimeException.class, () -> toggle.processAbility(defaultAbs.claimCreator()));
+ }
+}
\ No newline at end of file
diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/CustomToggleTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/CustomToggleTest.java
new file mode 100644
index 00000000..aeac3c8a
--- /dev/null
+++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/CustomToggleTest.java
@@ -0,0 +1,51 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.telegram.abilitybots.api.bot.DefaultAbilities;
+import org.telegram.abilitybots.api.bot.DefaultBot;
+import org.telegram.abilitybots.api.db.DBContext;
+
+import java.io.IOException;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
+
+class CustomToggleTest {
+ private DBContext db;
+ private AbilityToggle toggle;
+ private DefaultBot customBot;
+ private DefaultAbilities defaultAbs;
+
+ @BeforeEach
+ void setUp() {
+ db = offlineInstance("db");
+ defaultAbs = new DefaultAbilities(customBot);
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ db.clear();
+ db.close();
+ }
+
+ @Test
+ public void canTurnOffAbilities() {
+ toggle = new CustomToggle().turnOff(DefaultAbilities.CLAIM);
+ customBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+ assertFalse(customBot.abilities().containsKey(DefaultAbilities.CLAIM));
+ }
+
+ @Test
+ public void canProcessAbilities() {
+ String targetName = DefaultAbilities.CLAIM + "1toggle";
+ toggle = new CustomToggle().toggle(DefaultAbilities.CLAIM, targetName);
+ customBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+
+ assertTrue(customBot.abilities().containsKey(targetName));
+ }
+
+}
\ No newline at end of file
diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/DefaultToggleTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/DefaultToggleTest.java
new file mode 100644
index 00000000..8095d6fa
--- /dev/null
+++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/DefaultToggleTest.java
@@ -0,0 +1,69 @@
+package org.telegram.abilitybots.api.toggle;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.telegram.abilitybots.api.bot.DefaultAbilities;
+import org.telegram.abilitybots.api.bot.DefaultBot;
+import org.telegram.abilitybots.api.db.DBContext;
+import org.telegram.abilitybots.api.objects.Ability;
+
+import java.io.IOException;
+import java.util.Set;
+
+import static com.google.common.collect.Sets.newHashSet;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.telegram.abilitybots.api.bot.DefaultAbilities.*;
+import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
+
+class DefaultToggleTest {
+ private DBContext db;
+ private AbilityToggle toggle;
+ private DefaultBot defaultBot;
+ private DefaultAbilities defaultAbs;
+
+ @BeforeEach
+ void setUp() {
+ db = offlineInstance("db");
+ defaultAbs = new DefaultAbilities(defaultBot);
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ db.clear();
+ db.close();
+ }
+
+ @Test
+ public void claimsEveryAbilityIsOn() {
+ Ability random = DefaultBot.getDefaultBuilder()
+ .name("randomsomethingrandom").build();
+ toggle = new DefaultToggle();
+ defaultBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+
+ assertFalse(toggle.isOff(random));
+ }
+
+ @Test
+ public void passedSameAbilityRefOnProcess() {
+ Ability random = DefaultBot.getDefaultBuilder()
+ .name("randomsomethingrandom").build();
+ toggle = new DefaultToggle();
+ defaultBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+
+ assertSame(random, toggle.processAbility(random), "Toggle returned a different ability");
+ }
+
+ @Test
+ public void allAbilitiesAreRegistered() {
+ toggle = new DefaultToggle();
+ defaultBot = new DefaultBot(EMPTY, EMPTY, db, toggle);
+ Set defaultNames = newHashSet(
+ CLAIM, BAN, UNBAN,
+ PROMOTE, DEMOTE, RECOVER,
+ BACKUP, REPORT, COMMANDS);
+
+ assertTrue(defaultBot.abilities().keySet().containsAll(defaultNames), "Toggle returned a different ability");
+ }
+}
\ No newline at end of file