Add Abillity toggles and export default abilities to their own class

This commit is contained in:
Abbas Abou Daya 2019-09-29 23:00:16 -07:00
parent 6f0247232f
commit 861b7f24f9
18 changed files with 877 additions and 490 deletions

View File

@ -8,6 +8,7 @@
* [[Simple Example]] * [[Simple Example]]
* [[Hello Ability]] * [[Hello Ability]]
* [[Using Replies]] * [[Using Replies]]
* [[Ability Toggle]]
* [[Database Handling]] * [[Database Handling]]
* [[Bot Testing]] * [[Bot Testing]]
* [[Bot Recovery]] * [[Bot Recovery]]

View File

@ -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.

View File

@ -1,6 +1,8 @@
package org.telegram.abilitybots.api.bot; package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext; 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.meta.api.objects.Update;
import org.telegram.telegrambots.bots.DefaultBotOptions; import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.bots.TelegramLongPollingBot;
@ -16,18 +18,34 @@ import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
* @author Abbas Abou Daya * @author Abbas Abou Daya
*/ */
public abstract class AbilityBot extends BaseAbilityBot implements LongPollingBot { public abstract class AbilityBot extends BaseAbilityBot implements LongPollingBot {
protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) { protected AbilityBot(String botToken, String botUsername, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
super(botToken, botUsername, db, botOptions); super(botToken, botUsername, db, toggle, botOptions);
} }
protected AbilityBot(String botToken, String botUsername, DBContext db) { protected AbilityBot(String botToken, String botUsername, AbilityToggle toggle, DefaultBotOptions options) {
this(botToken, botUsername, db, new DefaultBotOptions()); 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) { protected AbilityBot(String botToken, String botUsername, DefaultBotOptions botOptions) {
this(botToken, botUsername, onlineInstance(botUsername), 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) { protected AbilityBot(String botToken, String botUsername) {
this(botToken, botUsername, onlineInstance(botUsername)); this(botToken, botUsername, onlineInstance(botUsername));
} }

View File

@ -1,6 +1,8 @@
package org.telegram.abilitybots.api.bot; package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext; 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.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.bots.DefaultBotOptions; import org.telegram.telegrambots.bots.DefaultBotOptions;
@ -21,19 +23,35 @@ public abstract class AbilityWebhookBot extends BaseAbilityBot implements Webhoo
private final String botPath; private final String botPath;
protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, DefaultBotOptions botOptions) { protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
super(botToken, botUsername, db, botOptions); super(botToken, botUsername, db, toggle, botOptions);
this.botPath = botPath; this.botPath = botPath;
} }
protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db) { protected AbilityWebhookBot(String botToken, String botUsername, String botPath, AbilityToggle toggle, DefaultBotOptions options) {
this(botToken, botUsername, botPath, db, new DefaultBotOptions()); 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) { protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DefaultBotOptions botOptions) {
this(botToken, botUsername, botPath, onlineInstance(botUsername), 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) { protected AbilityWebhookBot(String botToken, String botUsername, String botPath) {
this(botToken, botUsername, botPath, onlineInstance(botUsername)); this(botToken, botUsername, botPath, onlineInstance(botUsername));
} }

View File

@ -3,21 +3,15 @@ package org.telegram.abilitybots.api.bot;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.Ability; import org.telegram.abilitybots.api.objects.*;
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.sender.DefaultSender; import org.telegram.abilitybots.api.sender.DefaultSender;
import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.sender.SilentSender; 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.AbilityExtension;
import org.telegram.abilitybots.api.util.AbilityUtils; import org.telegram.abilitybots.api.util.AbilityUtils;
import org.telegram.abilitybots.api.util.Pair; 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.DefaultAbsSender;
import org.telegram.telegrambots.bots.DefaultBotOptions; import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot; 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.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.Message;
import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.User; 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.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.*;
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.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.lang.String.format;
import static java.time.ZonedDateTime.now; import static java.time.ZonedDateTime.now;
import static java.util.Arrays.stream; 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.Optional.ofNullable;
import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.compile; import static java.util.regex.Pattern.compile;
import static java.util.stream.Collectors.joining; import static org.telegram.abilitybots.api.objects.Locality.*;
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.MessageContext.newContext; 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.*;
import static org.telegram.abilitybots.api.objects.Privacy.CREATOR; import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
import static org.telegram.abilitybots.api.objects.Privacy.GROUP_ADMIN; import static org.telegram.abilitybots.api.util.AbilityUtils.*;
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;
/** /**
* The <b>father</b> of all ability bots. Bots that need to utilize abilities need to extend this bot. * The <b>father</b> 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 { public abstract class BaseAbilityBot extends DefaultAbsSender implements AbilityExtension {
private static final Logger log = LogManager.getLogger(BaseAbilityBot.class); private static final Logger log = LogManager.getLogger(BaseAbilityBot.class);
protected static final String DEFAULT = "default";
// DB objects // DB objects
public static final String ADMINS = "ADMINS"; public static final String ADMINS = "ADMINS";
public static final String USERS = "USERS"; public static final String USERS = "USERS";
public static final String USER_ID = "USER_ID"; public static final String USER_ID = "USER_ID";
public static final String BLACKLIST = "BLACKLIST"; 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 // DB and sender
protected final DBContext db; protected final DBContext db;
protected MessageSender sender; protected MessageSender sender;
protected SilentSender silent; protected SilentSender silent;
// Ability toggle
private final AbilityToggle toggle;
// Bot token and username // Bot token and username
private final String botToken; private final String botToken;
private final String botUsername; private final String botUsername;
@ -176,12 +108,13 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
public abstract int creatorId(); 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); super(botOptions);
this.botToken = botToken; this.botToken = botToken;
this.botUsername = botUsername; this.botUsername = botUsername;
this.db = db; this.db = db;
this.toggle = toggle;
this.sender = new DefaultSender(this); this.sender = new DefaultSender(this);
silent = new SilentSender(sender); silent = new SilentSender(sender);
@ -281,374 +214,6 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return true; 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;
}
}
/**
* <p>
* Format of the report:
* <p>
* [command1] - [description1]
* <p>
* [command2] - [description2]
* <p>
* ...
* <p>
* Once you invoke it, the bot will send the available commands to the chat. This is a public ability so anyone can invoke it.
* <p>
* Usage: <code>/commands</code>
*
* @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:
* <p>
* PUBLIC
* <p>
* [command1] - [description1]
* <p>
* [command2] - [description2]
* <p>
* GROUP_ADMIN
* <p>
* [command1] - [description1]
* <p>
* ...
*
* @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<Privacy, String> 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.
* <p>
* This is a high-profile ability and is restricted to the CREATOR only.
* <p>
* Usage: <code>/backup</code>
*
* @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)}.
* <p>
* The bot recovery process hugely depends on the implementation of the recovery method of {@link DBContext}.
* <p>
* Usage: <code>/recover</code>
*
* @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}.
* <p>
* Usage: <code>/ban @username</code>
* <p>
* <u>Note that admins who try to ban the creator, get banned.</u>
*
* @return the ability to ban the user from any kind of <b>bot interaction</b>
*/
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<Integer> 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: <code>/unban @username</code>
*
* @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<Integer> 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<Integer> 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<Integer> 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 <b>banned</b>.
*
* @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<Integer> 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<Message> send(String message, MessageContext ctx, String... args) {
return silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> sendMd(String message, MessageContext ctx, String... args) {
return silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> 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. * Registers the declared abilities using method reflection. Also, replies are accumulated using the built abilities and standalone methods that return a Reply.
* <p> * <p>
@ -665,11 +230,19 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
// Add the bot itself as it is an AbilityExtension // Add the bot itself as it is an AbilityExtension
extensions.add(this); extensions.add(this);
DefaultAbilities defaultAbs = new DefaultAbilities(this);
Stream<Ability> 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 // Extract all abilities from every single extension instance
abilities = extensions.stream() abilities = Stream.concat(defaultAbsStream,
extensions.stream()
.flatMap(ext -> stream(ext.getClass().getMethods()) .flatMap(ext -> stream(ext.getClass().getMethods())
.filter(checkReturnType(Ability.class)) .filter(checkReturnType(Ability.class))
.map(returnAbility(ext))) .map(returnAbility(ext))))
// Abilities are immutable, build it respectively // Abilities are immutable, build it respectively
.collect(ImmutableMap::<String, Ability>builder, .collect(ImmutableMap::<String, Ability>builder,
(b, a) -> b.put(a.name(), a), (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 * @param clazz the type to be tested
* @return a predicate testing the return type of the method corresponding to the class parameter * @return a predicate testing the return type of the method corresponding to the class parameter
*/ */
private Predicate<Method> checkReturnType(Class<?> clazz) { private static Predicate<Method> checkReturnType(Class<?> clazz) {
return method -> clazz.isAssignableFrom(method.getReturnType()); 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 * @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 * @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 -> { return method -> {
try { try {
return (Ability) method.invoke(obj); 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 * @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 * @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 -> { return method -> {
try { try {
return (Reply) method.invoke(obj); return (Reply) method.invoke(obj);
@ -833,7 +406,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
} }
@NotNull @NotNull
private Privacy getPrivacy(Update update, int id) { Privacy getPrivacy(Update update, int id) {
return isCreator(id) ? return isCreator(id) ?
CREATOR : isAdmin(id) ? CREATOR : isAdmin(id) ?
ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, 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() return ability.flags().stream()
.reduce(true, flagAnd, Boolean::logicalAnd); .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("_", "\\_");
}
} }

View File

@ -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;
}
/**
* <p>
* Format of the report:
* <p>
* [command1] - [description1]
* <p>
* [command2] - [description2]
* <p>
* ...
* <p>
* Once you invoke it, the bot will send the available commands to the chat. This is a public ability so anyone can invoke it.
* <p>
* Usage: <code>/commands</code>
*
* @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:
* <p>
* PUBLIC
* <p>
* [command1] - [description1]
* <p>
* [command2] - [description2]
* <p>
* GROUP_ADMIN
* <p>
* [command1] - [description1]
* <p>
* ...
*
* @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<Privacy, String> 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.
* <p>
* This is a high-profile ability and is restricted to the CREATOR only.
* <p>
* Usage: <code>/backup</code>
*
* @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)}.
* <p>
* The bot recovery process hugely depends on the implementation of the recovery method of {@link DBContext}.
* <p>
* Usage: <code>/recover</code>
*
* @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}.
* <p>
* Usage: <code>/ban @username</code>
* <p>
* <u>Note that admins who try to ban the creator, get banned.</u>
*
* @return the ability to ban the user from any kind of <b>bot interaction</b>
*/
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<Integer> 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: <code>/unban @username</code>
*
* @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<Integer> 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<Integer> 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<Integer> 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 <b>banned</b>.
*
* @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<Integer> 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<Message> send(String message, MessageContext ctx, String... args) {
return bot.silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> sendMd(String message, MessageContext ctx, String... args) {
return bot.silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> 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)));
}
}

View File

@ -147,8 +147,8 @@ public final class Ability {
private Privacy privacy; private Privacy privacy;
private Locality locality; private Locality locality;
private int argNum; private int argNum;
private Consumer<MessageContext> consumer; private Consumer<MessageContext> action;
private Consumer<MessageContext> postConsumer; private Consumer<MessageContext> postAction;
private List<Reply> replies; private List<Reply> replies;
private Predicate<Update>[] flags; private Predicate<Update>[] flags;
@ -157,7 +157,7 @@ public final class Ability {
} }
public AbilityBuilder action(Consumer<MessageContext> consumer) { public AbilityBuilder action(Consumer<MessageContext> consumer) {
this.consumer = consumer; this.action = consumer;
return this; return this;
} }
@ -191,8 +191,8 @@ public final class Ability {
return this; return this;
} }
public AbilityBuilder post(Consumer<MessageContext> postConsumer) { public AbilityBuilder post(Consumer<MessageContext> postAction) {
this.postConsumer = postConsumer; this.postAction = postAction;
return this; return this;
} }
@ -202,8 +202,21 @@ public final class Ability {
return this; 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() { 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);
} }
} }
} }

View File

@ -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);
}

View File

@ -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");
}
}

View File

@ -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<String, String> 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;
}
}

View File

@ -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;
}
}

View File

@ -244,4 +244,8 @@ public final class AbilityUtils {
return name.toString(); return name.toString();
} }
public static String escape(String username) {
return username.replace("_", "\\_");
}
} }

View File

@ -24,6 +24,7 @@ class AbilityBotI18nTest {
private DBContext db; private DBContext db;
private NoPublicCommandsBot bot; private NoPublicCommandsBot bot;
private DefaultAbilities defaultAbs;
private MessageSender sender; private MessageSender sender;
private SilentSender silent; private SilentSender silent;
@ -32,6 +33,7 @@ class AbilityBotI18nTest {
void setUp() { void setUp() {
db = offlineInstance("db"); db = offlineInstance("db");
bot = new NoPublicCommandsBot(EMPTY, EMPTY, db); bot = new NoPublicCommandsBot(EMPTY, EMPTY, db);
defaultAbs = new DefaultAbilities(bot);
sender = mock(MessageSender.class); sender = mock(MessageSender.class);
silent = mock(SilentSender.class); silent = mock(SilentSender.class);
@ -50,7 +52,7 @@ class AbilityBotI18nTest {
void missingPublicCommandsLocalizedInEnglishByDefault() { void missingPublicCommandsLocalizedInEnglishByDefault() {
MessageContext context = mockContext(NO_LANGUAGE_USER); MessageContext context = mockContext(NO_LANGUAGE_USER);
bot.reportCommands().action().accept(context); defaultAbs.reportCommands().action().accept(context);
verify(silent, times(1)) verify(silent, times(1))
.send("No available commands found.", NO_LANGUAGE_USER.getId()); .send("No available commands found.", NO_LANGUAGE_USER.getId());
@ -60,7 +62,7 @@ class AbilityBotI18nTest {
void missingPublicCommandsLocalizedInItalian() { void missingPublicCommandsLocalizedInItalian() {
MessageContext context = mockContext(ITALIAN_USER); MessageContext context = mockContext(ITALIAN_USER);
bot.reportCommands().action().accept(context); defaultAbs.reportCommands().action().accept(context);
verify(silent, times(1)) verify(silent, times(1))
.send("Non sono presenti comandi disponibile.", ITALIAN_USER.getId()); .send("Non sono presenti comandi disponibile.", ITALIAN_USER.getId());

View File

@ -69,6 +69,7 @@ public class AbilityBotTest {
public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null); public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null);
private DefaultBot bot; private DefaultBot bot;
private DefaultAbilities defaultAbs;
private DBContext db; private DBContext db;
private MessageSender sender; private MessageSender sender;
private SilentSender silent; private SilentSender silent;
@ -77,6 +78,7 @@ public class AbilityBotTest {
void setUp() { void setUp() {
db = offlineInstance("db"); db = offlineInstance("db");
bot = new DefaultBot(EMPTY, EMPTY, db); bot = new DefaultBot(EMPTY, EMPTY, db);
defaultAbs = new DefaultAbilities(bot);
sender = mock(MessageSender.class); sender = mock(MessageSender.class);
silent = mock(SilentSender.class); silent = mock(SilentSender.class);
@ -132,7 +134,7 @@ public class AbilityBotTest {
void canBackupDB() throws TelegramApiException { void canBackupDB() throws TelegramApiException {
MessageContext context = defaultContext(); MessageContext context = defaultContext();
bot.backupDB().action().accept(context); defaultAbs.backupDB().action().accept(context);
deleteQuietly(new java.io.File("backup.json")); deleteQuietly(new java.io.File("backup.json"));
verify(sender, times(1)).sendDocument(any()); verify(sender, times(1)).sendDocument(any());
@ -147,7 +149,7 @@ public class AbilityBotTest {
// Support for null parameter matching since due to mocking API changes // Support for null parameter matching since due to mocking API changes
when(sender.downloadFile(ArgumentMatchers.<File>isNull())).thenReturn(backupFile); when(sender.downloadFile(ArgumentMatchers.<File>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); 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"); 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(); MessageContext context = defaultContext();
bot.demoteAdmin().action().accept(context); defaultAbs.demoteAdmin().action().accept(context);
Set<Integer> actual = bot.admins(); Set<Integer> actual = bot.admins();
Set<Integer> expected = emptySet(); Set<Integer> expected = emptySet();
@ -182,7 +184,7 @@ public class AbilityBotTest {
MessageContext context = defaultContext(); MessageContext context = defaultContext();
bot.promoteAdmin().action().accept(context); defaultAbs.promoteAdmin().action().accept(context);
Set<Integer> actual = bot.admins(); Set<Integer> actual = bot.admins();
Set<Integer> expected = newHashSet(USER.getId()); Set<Integer> expected = newHashSet(USER.getId());
@ -194,7 +196,7 @@ public class AbilityBotTest {
addUsers(USER); addUsers(USER);
MessageContext context = defaultContext(); MessageContext context = defaultContext();
bot.banUser().action().accept(context); defaultAbs.banUser().action().accept(context);
Set<Integer> actual = bot.blacklist(); Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(USER.getId()); Set<Integer> expected = newHashSet(USER.getId());
@ -208,7 +210,7 @@ public class AbilityBotTest {
MessageContext context = defaultContext(); MessageContext context = defaultContext();
bot.unbanUser().action().accept(context); defaultAbs.unbanUser().action().accept(context);
Set<Integer> actual = bot.blacklist(); Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(); Set<Integer> expected = newHashSet();
@ -225,7 +227,7 @@ public class AbilityBotTest {
addUsers(USER, CREATOR); addUsers(USER, CREATOR);
MessageContext context = mockContext(USER, GROUP_ID, CREATOR.getUserName()); MessageContext context = mockContext(USER, GROUP_ID, CREATOR.getUserName());
bot.banUser().action().accept(context); defaultAbs.banUser().action().accept(context);
Set<Integer> actual = bot.blacklist(); Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(USER.getId()); Set<Integer> expected = newHashSet(USER.getId());
@ -243,7 +245,7 @@ public class AbilityBotTest {
void creatorCanClaimBot() { void creatorCanClaimBot() {
MessageContext context = mockContext(CREATOR, GROUP_ID); MessageContext context = mockContext(CREATOR, GROUP_ID);
bot.claimCreator().action().accept(context); defaultAbs.claimCreator().action().accept(context);
Set<Integer> actual = bot.admins(); Set<Integer> actual = bot.admins();
Set<Integer> expected = newHashSet(CREATOR.getId()); Set<Integer> expected = newHashSet(CREATOR.getId());
@ -544,7 +546,7 @@ public class AbilityBotTest {
void canReportCommands() { void canReportCommands() {
MessageContext context = mockContext(USER, GROUP_ID); 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); verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
} }
@ -578,7 +580,7 @@ public class AbilityBotTest {
when(message.hasText()).thenReturn(true); when(message.hasText()).thenReturn(true);
MessageContext creatorCtx = newContext(update, CREATOR, GROUP_ID); 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"; 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); verify(silent, times(1)).send(expected, GROUP_ID);
@ -595,7 +597,7 @@ public class AbilityBotTest {
MessageContext userCtx = newContext(update, USER, GROUP_ID); 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"; 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); verify(silent, times(1)).send(expected, GROUP_ID);

View File

@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.Ability; import org.telegram.abilitybots.api.objects.Ability;
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder; 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.Ability.builder;
import static org.telegram.abilitybots.api.objects.Flag.CALLBACK_QUERY; import static org.telegram.abilitybots.api.objects.Flag.CALLBACK_QUERY;
@ -17,6 +18,10 @@ public class DefaultBot extends AbilityBot {
super(token, username, db); super(token, username, db);
} }
public DefaultBot(String token, String username, DBContext db, AbilityToggle toggle) {
super(token, username, db, toggle);
}
public static AbilityBuilder getDefaultBuilder() { public static AbilityBuilder getDefaultBuilder() {
return builder() return builder()
.name("test") .name("test")

View File

@ -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()));
}
}

View File

@ -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));
}
}

View File

@ -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<String> defaultNames = newHashSet(
CLAIM, BAN, UNBAN,
PROMOTE, DEMOTE, RECOVER,
BACKUP, REPORT, COMMANDS);
assertTrue(defaultBot.abilities().keySet().containsAll(defaultNames), "Toggle returned a different ability");
}
}