From 861b7f24f904f0aeaac9f79f264b6437bca18471 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Sun, 29 Sep 2019 23:00:16 -0700 Subject: [PATCH] Add Abillity toggles and export default abilities to their own class --- TelegramBots.wiki/_Sidebar.md | 1 + TelegramBots.wiki/abilities/Ability-Toggle.md | 42 ++ .../abilitybots/api/bot/AbilityBot.java | 26 +- .../api/bot/AbilityWebhookBot.java | 26 +- .../abilitybots/api/bot/BaseAbilityBot.java | 490 +----------------- .../abilitybots/api/bot/DefaultAbilities.java | 435 ++++++++++++++++ .../abilitybots/api/objects/Ability.java | 25 +- .../abilitybots/api/toggle/AbilityToggle.java | 21 + .../api/toggle/BareboneToggle.java | 20 + .../abilitybots/api/toggle/CustomToggle.java | 56 ++ .../abilitybots/api/toggle/DefaultToggle.java | 19 + .../abilitybots/api/util/AbilityUtils.java | 4 + .../api/bot/AbilityBotI18nTest.java | 6 +- .../abilitybots/api/bot/AbilityBotTest.java | 24 +- .../abilitybots/api/bot/DefaultBot.java | 5 + .../api/toggle/BareboneToggleTest.java | 47 ++ .../api/toggle/CustomToggleTest.java | 51 ++ .../api/toggle/DefaultToggleTest.java | 69 +++ 18 files changed, 877 insertions(+), 490 deletions(-) create mode 100644 TelegramBots.wiki/abilities/Ability-Toggle.md create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/DefaultAbilities.java create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/AbilityToggle.java create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/BareboneToggle.java create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/CustomToggle.java create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/toggle/DefaultToggle.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/BareboneToggleTest.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/CustomToggleTest.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/toggle/DefaultToggleTest.java diff --git a/TelegramBots.wiki/_Sidebar.md b/TelegramBots.wiki/_Sidebar.md index f423f02e..4fe6b64e 100644 --- a/TelegramBots.wiki/_Sidebar.md +++ b/TelegramBots.wiki/_Sidebar.md @@ -8,6 +8,7 @@ * [[Simple Example]] * [[Hello Ability]] * [[Using Replies]] + * [[Ability Toggle]] * [[Database Handling]] * [[Bot Testing]] * [[Bot Recovery]] 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 d5f9c0ef..0ce5ab39 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), @@ -702,7 +275,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()); } @@ -729,7 +302,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability * @param obj an 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) { + private static Function returnAbility(Object obj) { return method -> { try { return (Ability) method.invoke(obj); @@ -746,7 +319,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability * @param obj an 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) { + private static Function returnReply(Object obj) { return method -> { try { return (Reply) method.invoke(obj); @@ -833,7 +406,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) ? @@ -938,13 +511,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 5494c595..3cf20a46 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 a1c5e8a5..a8bb77b8 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 @@ -69,6 +69,7 @@ public class AbilityBotTest { public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null); private DefaultBot bot; + private DefaultAbilities defaultAbs; private DBContext db; private MessageSender sender; private SilentSender silent; @@ -77,6 +78,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); @@ -132,7 +134,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()); @@ -147,7 +149,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"); @@ -169,7 +171,7 @@ public class AbilityBotTest { MessageContext context = defaultContext(); - bot.demoteAdmin().action().accept(context); + defaultAbs.demoteAdmin().action().accept(context); Set actual = bot.admins(); Set expected = emptySet(); @@ -182,7 +184,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()); @@ -194,7 +196,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()); @@ -208,7 +210,7 @@ public class AbilityBotTest { MessageContext context = defaultContext(); - bot.unbanUser().action().accept(context); + defaultAbs.unbanUser().action().accept(context); Set actual = bot.blacklist(); Set expected = newHashSet(); @@ -225,7 +227,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()); @@ -243,7 +245,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()); @@ -544,7 +546,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); } @@ -578,7 +580,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); @@ -595,7 +597,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