Merge branch 'webhook' of https://github.com/addo37/TelegramBots into addo37-webhook
# Conflicts: # telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java # telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java # telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java # telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramLongPollingBot.java # telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramWebhookBot.java
This commit is contained in:
commit
7daebe1318
@ -1,146 +1,23 @@
|
|||||||
package org.telegram.abilitybots.api.bot;
|
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.jetbrains.annotations.NotNull;
|
|
||||||
import org.telegram.abilitybots.api.db.DBContext;
|
import org.telegram.abilitybots.api.db.DBContext;
|
||||||
import org.telegram.abilitybots.api.objects.*;
|
import org.telegram.telegrambots.api.objects.Update;
|
||||||
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.util.AbilityUtils;
|
|
||||||
import org.telegram.abilitybots.api.util.Pair;
|
|
||||||
import org.telegram.abilitybots.api.util.Trio;
|
|
||||||
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.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.exceptions.TelegramApiException;
|
import org.telegram.telegrambots.exceptions.TelegramApiRequestException;
|
||||||
import org.telegram.telegrambots.meta.logging.BotLogger;
|
import org.telegram.telegrambots.generics.LongPollingBot;
|
||||||
|
import org.telegram.telegrambots.util.WebhookUtils;
|
||||||
|
|
||||||
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.*;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
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 jersey.repackaged.com.google.common.base.Throwables.propagate;
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isEmpty;
|
|
||||||
import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
|
import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
|
||||||
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.*;
|
|
||||||
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
|
||||||
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 <b>father</b> of all ability bots. Bots that need to utilize abilities need to extend this bot.
|
* The default AbilityBot class implements {@link LongPollingBot}. It delegates all updates to a {@link TelegramLongPollingBot} instance.
|
||||||
* <p>
|
|
||||||
* It's important to note that this bot strictly extends {@link TelegramLongPollingBot}.
|
|
||||||
* <p>
|
|
||||||
* All bots extending the {@link AbilityBot} get implicit abilities:
|
|
||||||
* <ul>
|
|
||||||
* <li>/claim - Claims this bot</li>
|
|
||||||
* <ul>
|
|
||||||
* <li>Sets the user as the {@link Privacy#CREATOR} of the bot</li>
|
|
||||||
* <li>Only the user with the ID returned by {@link AbilityBot#creatorId()} can genuinely claim the bot</li>
|
|
||||||
* </ul>
|
|
||||||
* <li>/report - reports all user-defined commands (abilities)</li>
|
|
||||||
* <ul>
|
|
||||||
* <li>The same format acceptable by BotFather</li>
|
|
||||||
* </ul>
|
|
||||||
* <li>/commands - returns a list of all possible bot commands based on the privacy of the requesting user</li>
|
|
||||||
* <li>/backup - returns a backup of the bot database</li>
|
|
||||||
* <li>/recover - recovers the database</li>
|
|
||||||
* <li>/promote <code>@username</code> - promotes user to bot admin</li>
|
|
||||||
* <li>/demote <code>@username</code> - demotes bot admin to user</li>
|
|
||||||
* <li>/ban <code>@username</code> - bans the user from accessing your bot commands and features</li>
|
|
||||||
* <li>/unban <code>@username</code> - lifts the ban from the user</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* Additional information of the implicit abilities are present in the methods that declare them.
|
|
||||||
* <p>
|
|
||||||
* The two most important handles in the AbilityBot are the {@link DBContext} <b><code>db</code></b> and the {@link MessageSender} <b><code>sender</code></b>.
|
|
||||||
* All bots extending AbilityBot can use both handles in their update consumers.
|
|
||||||
*
|
*
|
||||||
* @author Abbas Abou Daya
|
* @author Abbas Abou Daya
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "ConfusingArgumentToVarargsMethod", "ConstantConditions"})
|
public abstract class AbilityBot extends BaseAbilityBot implements LongPollingBot {
|
||||||
public abstract class AbilityBot extends TelegramLongPollingBot {
|
|
||||||
private static final String TAG = AbilityBot.class.getSimpleName();
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Bot token and username
|
|
||||||
private final String botToken;
|
|
||||||
private final String botUsername;
|
|
||||||
|
|
||||||
// Ability registry
|
|
||||||
private Map<String, Ability> abilities;
|
|
||||||
|
|
||||||
// Reply registry
|
|
||||||
private List<Reply> replies;
|
|
||||||
|
|
||||||
public abstract int creatorId();
|
|
||||||
|
|
||||||
protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
|
protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
|
||||||
super(botOptions);
|
super(botToken, botUsername, db, botOptions);
|
||||||
|
|
||||||
this.botToken = botToken;
|
|
||||||
this.botUsername = botUsername;
|
|
||||||
this.db = db;
|
|
||||||
this.sender = new DefaultSender(this);
|
|
||||||
silent = new SilentSender(sender);
|
|
||||||
|
|
||||||
registerAbilities();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AbilityBot(String botToken, String botUsername, DBContext db) {
|
protected AbilityBot(String botToken, String botUsername, DBContext db) {
|
||||||
@ -155,722 +32,13 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
|
|||||||
this(botToken, botUsername, onlineInstance(botUsername));
|
this(botToken, botUsername, onlineInstance(botUsername));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the map of ID -> User
|
|
||||||
*/
|
|
||||||
protected Map<Integer, User> users() {
|
|
||||||
return db.getMap(USERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the map of Username -> ID
|
|
||||||
*/
|
|
||||||
protected Map<String, Integer> userIds() {
|
|
||||||
return db.getMap(USER_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a blacklist containing all the IDs of the banned users
|
|
||||||
*/
|
|
||||||
protected Set<Integer> blacklist() {
|
|
||||||
return db.getSet(BLACKLIST);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return an admin set of all the IDs of bot administrators
|
|
||||||
*/
|
|
||||||
protected Set<Integer> admins() {
|
|
||||||
return db.getSet(ADMINS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the immutable map of String -> Ability
|
|
||||||
*/
|
|
||||||
public Map<String, Ability> abilities() {
|
|
||||||
return abilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the immutable list carrying the embedded replies
|
|
||||||
*/
|
|
||||||
public List<Reply> replies() {
|
|
||||||
return replies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method contains the stream of actions that are applied on any update.
|
|
||||||
* <p>
|
|
||||||
* It will correctly handle addition of users into the DB and the execution of abilities and replies.
|
|
||||||
*
|
|
||||||
* @param update the update received by Telegram's API
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpdateReceived(Update update) {
|
public void onUpdateReceived(Update update) {
|
||||||
BotLogger.info(format("New update [%s] received at %s", update.getUpdateId(), now()), format("%s - %s", TAG, botUsername));
|
super.onUpdateReceived(update);
|
||||||
BotLogger.info(update.toString(), TAG);
|
|
||||||
long millisStarted = System.currentTimeMillis();
|
|
||||||
|
|
||||||
Stream.of(update)
|
|
||||||
.filter(this::checkGlobalFlags)
|
|
||||||
.filter(this::checkBlacklist)
|
|
||||||
.map(this::addUser)
|
|
||||||
.filter(this::filterReply)
|
|
||||||
.map(this::getAbility)
|
|
||||||
.filter(this::validateAbility)
|
|
||||||
.filter(this::checkPrivacy)
|
|
||||||
.filter(this::checkLocality)
|
|
||||||
.filter(this::checkInput)
|
|
||||||
.filter(this::checkMessageFlags)
|
|
||||||
.map(this::getContext)
|
|
||||||
.map(this::consumeUpdate)
|
|
||||||
.forEach(this::postConsumption);
|
|
||||||
|
|
||||||
long processingTime = System.currentTimeMillis() - millisStarted;
|
|
||||||
BotLogger.info(format("Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", update.getUpdateId(), now(), processingTime), format("%s - %s", TAG, botUsername));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBotToken() {
|
public void clearWebhook() throws TelegramApiRequestException {
|
||||||
return botToken;
|
WebhookUtils.clearWebhook(this);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBotUsername() {
|
|
||||||
return botUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the update against the provided global flags. The default implementation is a passthrough to all updates.
|
|
||||||
* <p>
|
|
||||||
* This method should be <b>overridden</b> if the user wants to restrict bot usage to only certain updates.
|
|
||||||
*
|
|
||||||
* @param update a Telegram {@link Update}
|
|
||||||
* @return <tt>true</tt> if the update satisfies the global flags
|
|
||||||
*/
|
|
||||||
protected boolean checkGlobalFlags(Update update) {
|
|
||||||
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 propagate(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.entrySet().stream()
|
|
||||||
.filter(entry -> nonNull(entry.getValue().info()))
|
|
||||||
.map(entry -> {
|
|
||||||
String name = entry.getValue().name();
|
|
||||||
String info = entry.getValue().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.entrySet().stream()
|
|
||||||
.map(entry -> {
|
|
||||||
String name = entry.getValue().name();
|
|
||||||
String info = entry.getValue().info();
|
|
||||||
|
|
||||||
if (!isEmpty(info))
|
|
||||||
return Pair.of(entry.getValue().privacy(), format("/%s - %s", name, info));
|
|
||||||
return Pair.of(entry.getValue().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) {
|
|
||||||
BotLogger.error("Error while fetching backup", TAG, e);
|
|
||||||
} catch (TelegramApiException e) {
|
|
||||||
BotLogger.error("Error while sending document/backup file", TAG, 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) {
|
|
||||||
BotLogger.error("Could not recover DB from backup", TAG, 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 AbilityBot#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.
|
|
||||||
* <p>
|
|
||||||
* <b>Only abilities and replies with the <u>public</u> accessor are registered!</b>
|
|
||||||
*/
|
|
||||||
private void registerAbilities() {
|
|
||||||
try {
|
|
||||||
abilities = stream(this.getClass().getMethods())
|
|
||||||
.filter(method -> method.getReturnType().equals(Ability.class))
|
|
||||||
.map(this::returnAbility)
|
|
||||||
.collect(ImmutableMap::<String, Ability>builder,
|
|
||||||
(b, a) -> b.put(a.name(), a),
|
|
||||||
(b1, b2) -> b1.putAll(b2.build()))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Stream<Reply> methodReplies = stream(this.getClass().getMethods())
|
|
||||||
.filter(method -> method.getReturnType().equals(Reply.class))
|
|
||||||
.map(this::returnReply);
|
|
||||||
|
|
||||||
Stream<Reply> abilityReplies = abilities.values().stream()
|
|
||||||
.flatMap(ability -> ability.replies().stream());
|
|
||||||
|
|
||||||
replies = Stream.concat(methodReplies, abilityReplies).collect(
|
|
||||||
ImmutableList::<Reply>builder,
|
|
||||||
Builder::add,
|
|
||||||
(b1, b2) -> b1.addAll(b2.build()))
|
|
||||||
.build();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
BotLogger.error(TAG, "Duplicate names found while registering abilities. Make sure that the abilities declared don't clash with the reserved ones.", e);
|
|
||||||
throw propagate(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the method and retrieves its return {@link Ability}.
|
|
||||||
*
|
|
||||||
* @param method a method that returns an ability
|
|
||||||
* @return the ability returned by the method
|
|
||||||
*/
|
|
||||||
private Ability returnAbility(Method method) {
|
|
||||||
try {
|
|
||||||
return (Ability) method.invoke(this);
|
|
||||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
||||||
BotLogger.error("Could not add ability", TAG, e);
|
|
||||||
throw propagate(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the method and retrieves its returned Reply.
|
|
||||||
*
|
|
||||||
* @param method a method that returns a reply
|
|
||||||
* @return the reply returned by the method
|
|
||||||
*/
|
|
||||||
private Reply returnReply(Method method) {
|
|
||||||
try {
|
|
||||||
return (Reply) method.invoke(this);
|
|
||||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
||||||
BotLogger.error("Could not add reply", TAG, e);
|
|
||||||
throw propagate(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postConsumption(Pair<MessageContext, Ability> pair) {
|
|
||||||
ofNullable(pair.b().postAction())
|
|
||||||
.ifPresent(consumer -> consumer.accept(pair.a()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<MessageContext, Ability> consumeUpdate(Pair<MessageContext, Ability> pair) {
|
|
||||||
pair.b().action().accept(pair.a());
|
|
||||||
return pair;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
|
|
||||||
Update update = trio.a();
|
|
||||||
User user = AbilityUtils.getUser(update);
|
|
||||||
|
|
||||||
return Pair.of(newContext(update, user, getChatId(update), trio.c()), trio.b());
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkBlacklist(Update update) {
|
|
||||||
Integer id = AbilityUtils.getUser(update).getId();
|
|
||||||
|
|
||||||
return id == creatorId() || !blacklist().contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkInput(Trio<Update, Ability, String[]> trio) {
|
|
||||||
String[] tokens = trio.c();
|
|
||||||
int abilityTokens = trio.b().tokens();
|
|
||||||
|
|
||||||
boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens);
|
|
||||||
|
|
||||||
if (!isOk)
|
|
||||||
silent.send(
|
|
||||||
getLocalizedMessage(
|
|
||||||
CHECK_INPUT_FAIL,
|
|
||||||
AbilityUtils.getUser(trio.a()).getLanguageCode(),
|
|
||||||
abilityTokens, abilityTokens == 1 ? "input" : "inputs"),
|
|
||||||
getChatId(trio.a()));
|
|
||||||
return isOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkLocality(Trio<Update, Ability, String[]> trio) {
|
|
||||||
Update update = trio.a();
|
|
||||||
Locality locality = isUserMessage(update) ? USER : GROUP;
|
|
||||||
Locality abilityLocality = trio.b().locality();
|
|
||||||
|
|
||||||
boolean isOk = abilityLocality == ALL || locality == abilityLocality;
|
|
||||||
|
|
||||||
if (!isOk)
|
|
||||||
silent.send(
|
|
||||||
getLocalizedMessage(
|
|
||||||
CHECK_LOCALITY_FAIL,
|
|
||||||
AbilityUtils.getUser(trio.a()).getLanguageCode(),
|
|
||||||
abilityLocality.toString().toLowerCase()),
|
|
||||||
getChatId(trio.a()));
|
|
||||||
return isOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkPrivacy(Trio<Update, Ability, String[]> trio) {
|
|
||||||
Update update = trio.a();
|
|
||||||
User user = AbilityUtils.getUser(update);
|
|
||||||
Privacy privacy;
|
|
||||||
int id = user.getId();
|
|
||||||
|
|
||||||
privacy = getPrivacy(update, id);
|
|
||||||
|
|
||||||
boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
|
|
||||||
|
|
||||||
if (!isOk)
|
|
||||||
silent.send(
|
|
||||||
getLocalizedMessage(
|
|
||||||
CHECK_PRIVACY_FAIL,
|
|
||||||
AbilityUtils.getUser(trio.a()).getLanguageCode()),
|
|
||||||
getChatId(trio.a()));
|
|
||||||
return isOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Privacy getPrivacy(Update update, int id) {
|
|
||||||
return isCreator(id) ?
|
|
||||||
CREATOR : isAdmin(id) ?
|
|
||||||
ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ?
|
|
||||||
GROUP_ADMIN : PUBLIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isGroupAdmin(Update update, int id) {
|
|
||||||
GetChatAdministrators admins = new GetChatAdministrators().setChatId(getChatId(update));
|
|
||||||
|
|
||||||
return silent.execute(admins)
|
|
||||||
.orElse(new ArrayList<>()).stream()
|
|
||||||
.anyMatch(member -> member.getUser().getId() == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCreator(int id) {
|
|
||||||
return id == creatorId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAdmin(Integer id) {
|
|
||||||
return admins().contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean validateAbility(Trio<Update, Ability, String[]> trio) {
|
|
||||||
return trio.b() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Trio<Update, Ability, String[]> getAbility(Update update) {
|
|
||||||
// Handle updates without messages
|
|
||||||
// Passing through this function means that the global flags have passed
|
|
||||||
Message msg = update.getMessage();
|
|
||||||
if (!update.hasMessage() || !msg.hasText())
|
|
||||||
return Trio.of(update, abilities.get(DEFAULT), new String[]{});
|
|
||||||
|
|
||||||
String[] tokens = msg.getText().split(" ");
|
|
||||||
|
|
||||||
if (tokens[0].startsWith("/")) {
|
|
||||||
String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase();
|
|
||||||
Ability ability = abilities.get(abilityToken);
|
|
||||||
tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
|
|
||||||
return Trio.of(update, ability, tokens);
|
|
||||||
} else {
|
|
||||||
Ability ability = abilities.get(DEFAULT);
|
|
||||||
return Trio.of(update, ability, tokens);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String stripBotUsername(String token) {
|
|
||||||
return compile(format("@%s", botUsername), CASE_INSENSITIVE)
|
|
||||||
.matcher(token)
|
|
||||||
.replaceAll("");
|
|
||||||
}
|
|
||||||
|
|
||||||
Update addUser(Update update) {
|
|
||||||
User endUser = AbilityUtils.getUser(update);
|
|
||||||
|
|
||||||
users().compute(endUser.getId(), (id, user) -> {
|
|
||||||
if (user == null) {
|
|
||||||
updateUserId(user, endUser);
|
|
||||||
return endUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.equals(endUser)) {
|
|
||||||
updateUserId(user, endUser);
|
|
||||||
return endUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
db.commit();
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUserId(User oldUser, User newUser) {
|
|
||||||
if (oldUser != null && oldUser.getUserName() != null) {
|
|
||||||
// Remove old username -> ID
|
|
||||||
userIds().remove(oldUser.getUserName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newUser.getUserName() != null) {
|
|
||||||
// Add new mapping with the new username
|
|
||||||
userIds().put(newUser.getUserName().toLowerCase(), newUser.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean filterReply(Update update) {
|
|
||||||
return replies.stream()
|
|
||||||
.filter(reply -> reply.isOkFor(update))
|
|
||||||
.map(reply -> {
|
|
||||||
reply.actOn(update);
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.reduce(true, Boolean::logicalAnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkMessageFlags(Trio<Update, Ability, String[]> trio) {
|
|
||||||
Ability ability = trio.b();
|
|
||||||
Update update = trio.a();
|
|
||||||
|
|
||||||
// The following variable is required to avoid bug #JDK-8044546
|
|
||||||
BiFunction<Boolean, Predicate<Update>, Boolean> flagAnd = (flag, nextFlag) -> flag && nextFlag.test(update);
|
|
||||||
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("_", "\\_");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package org.telegram.abilitybots.api.bot;
|
||||||
|
|
||||||
|
import org.telegram.abilitybots.api.db.DBContext;
|
||||||
|
import org.telegram.telegrambots.api.methods.BotApiMethod;
|
||||||
|
import org.telegram.telegrambots.api.objects.Update;
|
||||||
|
import org.telegram.telegrambots.bots.DefaultBotOptions;
|
||||||
|
import org.telegram.telegrambots.bots.TelegramWebhookBot;
|
||||||
|
import org.telegram.telegrambots.exceptions.TelegramApiRequestException;
|
||||||
|
import org.telegram.telegrambots.generics.WebhookBot;
|
||||||
|
import org.telegram.telegrambots.util.WebhookUtils;
|
||||||
|
|
||||||
|
import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link WebhookBot}-flavor AbilityBot. It delegates all updates to a {@link TelegramWebhookBot} instance.
|
||||||
|
*
|
||||||
|
* @author Abbas Abou Daya
|
||||||
|
*/
|
||||||
|
public abstract class AbilityWebhookBot extends BaseAbilityBot implements WebhookBot {
|
||||||
|
|
||||||
|
private final String botPath;
|
||||||
|
|
||||||
|
protected AbilityWebhookBot(String botToken, String botUsername, String botPath, DBContext db, DefaultBotOptions botOptions) {
|
||||||
|
super(botToken, botUsername, db, 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, DefaultBotOptions botOptions) {
|
||||||
|
this(botToken, botUsername, botPath, onlineInstance(botUsername), botOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbilityWebhookBot(String botToken, String botUsername, String botPath) {
|
||||||
|
this(botToken, botUsername, botPath, onlineInstance(botUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BotApiMethod onWebhookUpdateReceived(Update update) {
|
||||||
|
super.onUpdateReceived(update);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebhook(String url, String publicCertificatePath) throws TelegramApiRequestException {
|
||||||
|
WebhookUtils.setWebhook(this, url, publicCertificatePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBotPath() {
|
||||||
|
return botPath;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,861 @@
|
|||||||
|
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.jetbrains.annotations.NotNull;
|
||||||
|
import org.telegram.abilitybots.api.db.DBContext;
|
||||||
|
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.util.AbilityUtils;
|
||||||
|
import org.telegram.abilitybots.api.util.Pair;
|
||||||
|
import org.telegram.abilitybots.api.util.Trio;
|
||||||
|
import org.telegram.telegrambots.api.methods.GetFile;
|
||||||
|
import org.telegram.telegrambots.api.methods.groupadministration.GetChatAdministrators;
|
||||||
|
import org.telegram.telegrambots.api.methods.send.SendDocument;
|
||||||
|
import org.telegram.telegrambots.api.objects.Message;
|
||||||
|
import org.telegram.telegrambots.api.objects.Update;
|
||||||
|
import org.telegram.telegrambots.api.objects.User;
|
||||||
|
import org.telegram.telegrambots.bots.DefaultAbsSender;
|
||||||
|
import org.telegram.telegrambots.bots.DefaultBotOptions;
|
||||||
|
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
||||||
|
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||||
|
import org.telegram.telegrambots.logging.BotLogger;
|
||||||
|
|
||||||
|
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.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
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 jersey.repackaged.com.google.common.base.Throwables.propagate;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isEmpty;
|
||||||
|
import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
|
||||||
|
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.*;
|
||||||
|
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
||||||
|
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 <b>father</b> of all ability bots. Bots that need to utilize abilities need to extend this bot.
|
||||||
|
* <p>
|
||||||
|
* It's important to note that this bot strictly extends {@link TelegramLongPollingBot}.
|
||||||
|
* <p>
|
||||||
|
* All bots extending the {@link BaseAbilityBot} get implicit abilities:
|
||||||
|
* <ul>
|
||||||
|
* <li>/claim - Claims this bot</li>
|
||||||
|
* <ul>
|
||||||
|
* <li>Sets the user as the {@link Privacy#CREATOR} of the bot</li>
|
||||||
|
* <li>Only the user with the ID returned by {@link BaseAbilityBot#creatorId()} can genuinely claim the bot</li>
|
||||||
|
* </ul>
|
||||||
|
* <li>/report - reports all user-defined commands (abilities)</li>
|
||||||
|
* <ul>
|
||||||
|
* <li>The same format acceptable by BotFather</li>
|
||||||
|
* </ul>
|
||||||
|
* <li>/commands - returns a list of all possible bot commands based on the privacy of the requesting user</li>
|
||||||
|
* <li>/backup - returns a backup of the bot database</li>
|
||||||
|
* <li>/recover - recovers the database</li>
|
||||||
|
* <li>/promote <code>@username</code> - promotes user to bot admin</li>
|
||||||
|
* <li>/demote <code>@username</code> - demotes bot admin to user</li>
|
||||||
|
* <li>/ban <code>@username</code> - bans the user from accessing your bot commands and features</li>
|
||||||
|
* <li>/unban <code>@username</code> - lifts the ban from the user</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* Additional information of the implicit abilities are present in the methods that declare them.
|
||||||
|
* <p>
|
||||||
|
* The two most important handles in the BaseAbilityBot are the {@link DBContext} <b><code>db</code></b> and the {@link MessageSender} <b><code>sender</code></b>.
|
||||||
|
* All bots extending BaseAbilityBot can use both handles in their update consumers.
|
||||||
|
*
|
||||||
|
* @author Abbas Abou Daya
|
||||||
|
*/
|
||||||
|
public abstract class BaseAbilityBot extends DefaultAbsSender {
|
||||||
|
private static final String TAG = BaseAbilityBot.class.getSimpleName();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Bot token and username
|
||||||
|
private final String botToken;
|
||||||
|
private final String botUsername;
|
||||||
|
|
||||||
|
// Ability registry
|
||||||
|
private Map<String, Ability> abilities;
|
||||||
|
|
||||||
|
// Reply registry
|
||||||
|
private List<Reply> replies;
|
||||||
|
|
||||||
|
public abstract int creatorId();
|
||||||
|
|
||||||
|
protected BaseAbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
|
||||||
|
super(botOptions);
|
||||||
|
|
||||||
|
this.botToken = botToken;
|
||||||
|
this.botUsername = botUsername;
|
||||||
|
this.db = db;
|
||||||
|
this.sender = new DefaultSender(this);
|
||||||
|
silent = new SilentSender(sender);
|
||||||
|
|
||||||
|
registerAbilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the map of ID -> User
|
||||||
|
*/
|
||||||
|
protected Map<Integer, User> users() {
|
||||||
|
return db.getMap(USERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the map of Username -> ID
|
||||||
|
*/
|
||||||
|
protected Map<String, Integer> userIds() {
|
||||||
|
return db.getMap(USER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a blacklist containing all the IDs of the banned users
|
||||||
|
*/
|
||||||
|
protected Set<Integer> blacklist() {
|
||||||
|
return db.getSet(BLACKLIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an admin set of all the IDs of bot administrators
|
||||||
|
*/
|
||||||
|
protected Set<Integer> admins() {
|
||||||
|
return db.getSet(ADMINS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the immutable map of String -> Ability
|
||||||
|
*/
|
||||||
|
public Map<String, Ability> abilities() {
|
||||||
|
return abilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the immutable list carrying the embedded replies
|
||||||
|
*/
|
||||||
|
public List<Reply> replies() {
|
||||||
|
return replies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method contains the stream of actions that are applied on any update.
|
||||||
|
* <p>
|
||||||
|
* It will correctly handle addition of users into the DB and the execution of abilities and replies.
|
||||||
|
*
|
||||||
|
* @param update the update received by Telegram's API
|
||||||
|
*/
|
||||||
|
public void onUpdateReceived(Update update) {
|
||||||
|
BotLogger.info(format("New update [%s] received at %s", update.getUpdateId(), now()), format("%s - %s", TAG, botUsername));
|
||||||
|
BotLogger.info(update.toString(), TAG);
|
||||||
|
long millisStarted = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Stream.of(update)
|
||||||
|
.filter(this::checkGlobalFlags)
|
||||||
|
.filter(this::checkBlacklist)
|
||||||
|
.map(this::addUser)
|
||||||
|
.filter(this::filterReply)
|
||||||
|
.map(this::getAbility)
|
||||||
|
.filter(this::validateAbility)
|
||||||
|
.filter(this::checkPrivacy)
|
||||||
|
.filter(this::checkLocality)
|
||||||
|
.filter(this::checkInput)
|
||||||
|
.filter(this::checkMessageFlags)
|
||||||
|
.map(this::getContext)
|
||||||
|
.map(this::consumeUpdate)
|
||||||
|
.forEach(this::postConsumption);
|
||||||
|
|
||||||
|
long processingTime = System.currentTimeMillis() - millisStarted;
|
||||||
|
BotLogger.info(format("Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", update.getUpdateId(), now(), processingTime), format("%s - %s", TAG, botUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBotToken() {
|
||||||
|
return botToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBotUsername() {
|
||||||
|
return botUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the update against the provided global flags. The default implementation is a passthrough to all updates.
|
||||||
|
* <p>
|
||||||
|
* This method should be <b>overridden</b> if the user wants to restrict bot usage to only certain updates.
|
||||||
|
*
|
||||||
|
* @param update a Telegram {@link Update}
|
||||||
|
* @return <tt>true</tt> if the update satisfies the global flags
|
||||||
|
*/
|
||||||
|
protected boolean checkGlobalFlags(Update update) {
|
||||||
|
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 propagate(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.entrySet().stream()
|
||||||
|
.filter(entry -> nonNull(entry.getValue().info()))
|
||||||
|
.map(entry -> {
|
||||||
|
String name = entry.getValue().name();
|
||||||
|
String info = entry.getValue().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.entrySet().stream()
|
||||||
|
.map(entry -> {
|
||||||
|
String name = entry.getValue().name();
|
||||||
|
String info = entry.getValue().info();
|
||||||
|
|
||||||
|
if (!isEmpty(info))
|
||||||
|
return Pair.of(entry.getValue().privacy(), format("/%s - %s", name, info));
|
||||||
|
return Pair.of(entry.getValue().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()
|
||||||
|
.setNewDocument(backup)
|
||||||
|
.setChatId(ctx.chatId())
|
||||||
|
);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
BotLogger.error("Error while fetching backup", TAG, e);
|
||||||
|
} catch (TelegramApiException e) {
|
||||||
|
BotLogger.error("Error while sending document/backup file", TAG, 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) {
|
||||||
|
BotLogger.error("Could not recover DB from backup", TAG, 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.
|
||||||
|
* <p>
|
||||||
|
* <b>Only abilities and replies with the <u>public</u> accessor are registered!</b>
|
||||||
|
*/
|
||||||
|
private void registerAbilities() {
|
||||||
|
try {
|
||||||
|
abilities = stream(this.getClass().getMethods())
|
||||||
|
.filter(method -> method.getReturnType().equals(Ability.class))
|
||||||
|
.map(this::returnAbility)
|
||||||
|
.collect(ImmutableMap::<String, Ability>builder,
|
||||||
|
(b, a) -> b.put(a.name(), a),
|
||||||
|
(b1, b2) -> b1.putAll(b2.build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Stream<Reply> methodReplies = stream(this.getClass().getMethods())
|
||||||
|
.filter(method -> method.getReturnType().equals(Reply.class))
|
||||||
|
.map(this::returnReply);
|
||||||
|
|
||||||
|
Stream<Reply> abilityReplies = abilities.values().stream()
|
||||||
|
.flatMap(ability -> ability.replies().stream());
|
||||||
|
|
||||||
|
replies = Stream.concat(methodReplies, abilityReplies).collect(
|
||||||
|
ImmutableList::<Reply>builder,
|
||||||
|
Builder::add,
|
||||||
|
(b1, b2) -> b1.addAll(b2.build()))
|
||||||
|
.build();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
BotLogger.error(TAG, "Duplicate names found while registering abilities. Make sure that the abilities declared don't clash with the reserved ones.", e);
|
||||||
|
throw propagate(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the method and retrieves its return {@link Ability}.
|
||||||
|
*
|
||||||
|
* @param method a method that returns an ability
|
||||||
|
* @return the ability returned by the method
|
||||||
|
*/
|
||||||
|
private Ability returnAbility(Method method) {
|
||||||
|
try {
|
||||||
|
return (Ability) method.invoke(this);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
BotLogger.error("Could not add ability", TAG, e);
|
||||||
|
throw propagate(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the method and retrieves its returned Reply.
|
||||||
|
*
|
||||||
|
* @param method a method that returns a reply
|
||||||
|
* @return the reply returned by the method
|
||||||
|
*/
|
||||||
|
private Reply returnReply(Method method) {
|
||||||
|
try {
|
||||||
|
return (Reply) method.invoke(this);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
BotLogger.error("Could not add reply", TAG, e);
|
||||||
|
throw propagate(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postConsumption(Pair<MessageContext, Ability> pair) {
|
||||||
|
ofNullable(pair.b().postAction())
|
||||||
|
.ifPresent(consumer -> consumer.accept(pair.a()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair<MessageContext, Ability> consumeUpdate(Pair<MessageContext, Ability> pair) {
|
||||||
|
pair.b().action().accept(pair.a());
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
|
||||||
|
Update update = trio.a();
|
||||||
|
User user = AbilityUtils.getUser(update);
|
||||||
|
|
||||||
|
return Pair.of(newContext(update, user, getChatId(update), trio.c()), trio.b());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean checkBlacklist(Update update) {
|
||||||
|
Integer id = AbilityUtils.getUser(update).getId();
|
||||||
|
|
||||||
|
return id == creatorId() || !blacklist().contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean checkInput(Trio<Update, Ability, String[]> trio) {
|
||||||
|
String[] tokens = trio.c();
|
||||||
|
int abilityTokens = trio.b().tokens();
|
||||||
|
|
||||||
|
boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens);
|
||||||
|
|
||||||
|
if (!isOk)
|
||||||
|
silent.send(
|
||||||
|
getLocalizedMessage(
|
||||||
|
CHECK_INPUT_FAIL,
|
||||||
|
AbilityUtils.getUser(trio.a()).getLanguageCode(),
|
||||||
|
abilityTokens, abilityTokens == 1 ? "input" : "inputs"),
|
||||||
|
getChatId(trio.a()));
|
||||||
|
return isOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean checkLocality(Trio<Update, Ability, String[]> trio) {
|
||||||
|
Update update = trio.a();
|
||||||
|
Locality locality = isUserMessage(update) ? USER : GROUP;
|
||||||
|
Locality abilityLocality = trio.b().locality();
|
||||||
|
|
||||||
|
boolean isOk = abilityLocality == ALL || locality == abilityLocality;
|
||||||
|
|
||||||
|
if (!isOk)
|
||||||
|
silent.send(
|
||||||
|
getLocalizedMessage(
|
||||||
|
CHECK_LOCALITY_FAIL,
|
||||||
|
AbilityUtils.getUser(trio.a()).getLanguageCode(),
|
||||||
|
abilityLocality.toString().toLowerCase()),
|
||||||
|
getChatId(trio.a()));
|
||||||
|
return isOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean checkPrivacy(Trio<Update, Ability, String[]> trio) {
|
||||||
|
Update update = trio.a();
|
||||||
|
User user = AbilityUtils.getUser(update);
|
||||||
|
Privacy privacy;
|
||||||
|
int id = user.getId();
|
||||||
|
|
||||||
|
privacy = getPrivacy(update, id);
|
||||||
|
|
||||||
|
boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
|
||||||
|
|
||||||
|
if (!isOk)
|
||||||
|
silent.send(
|
||||||
|
getLocalizedMessage(
|
||||||
|
CHECK_PRIVACY_FAIL,
|
||||||
|
AbilityUtils.getUser(trio.a()).getLanguageCode()),
|
||||||
|
getChatId(trio.a()));
|
||||||
|
return isOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Privacy getPrivacy(Update update, int id) {
|
||||||
|
return isCreator(id) ?
|
||||||
|
CREATOR : isAdmin(id) ?
|
||||||
|
ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ?
|
||||||
|
GROUP_ADMIN : PUBLIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGroupAdmin(Update update, int id) {
|
||||||
|
GetChatAdministrators admins = new GetChatAdministrators().setChatId(getChatId(update));
|
||||||
|
|
||||||
|
return silent.execute(admins)
|
||||||
|
.orElse(new ArrayList<>()).stream()
|
||||||
|
.anyMatch(member -> member.getUser().getId() == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCreator(int id) {
|
||||||
|
return id == creatorId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAdmin(Integer id) {
|
||||||
|
return admins().contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean validateAbility(Trio<Update, Ability, String[]> trio) {
|
||||||
|
return trio.b() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trio<Update, Ability, String[]> getAbility(Update update) {
|
||||||
|
// Handle updates without messages
|
||||||
|
// Passing through this function means that the global flags have passed
|
||||||
|
Message msg = update.getMessage();
|
||||||
|
if (!update.hasMessage() || !msg.hasText())
|
||||||
|
return Trio.of(update, abilities.get(DEFAULT), new String[]{});
|
||||||
|
|
||||||
|
String[] tokens = msg.getText().split(" ");
|
||||||
|
|
||||||
|
if (tokens[0].startsWith("/")) {
|
||||||
|
String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase();
|
||||||
|
Ability ability = abilities.get(abilityToken);
|
||||||
|
tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
|
||||||
|
return Trio.of(update, ability, tokens);
|
||||||
|
} else {
|
||||||
|
Ability ability = abilities.get(DEFAULT);
|
||||||
|
return Trio.of(update, ability, tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripBotUsername(String token) {
|
||||||
|
return compile(format("@%s", botUsername), CASE_INSENSITIVE)
|
||||||
|
.matcher(token)
|
||||||
|
.replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
|
Update addUser(Update update) {
|
||||||
|
User endUser = AbilityUtils.getUser(update);
|
||||||
|
|
||||||
|
users().compute(endUser.getId(), (id, user) -> {
|
||||||
|
if (user == null) {
|
||||||
|
updateUserId(user, endUser);
|
||||||
|
return endUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.equals(endUser)) {
|
||||||
|
updateUserId(user, endUser);
|
||||||
|
return endUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
|
db.commit();
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUserId(User oldUser, User newUser) {
|
||||||
|
if (oldUser != null && oldUser.getUserName() != null) {
|
||||||
|
// Remove old username -> ID
|
||||||
|
userIds().remove(oldUser.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUser.getUserName() != null) {
|
||||||
|
// Add new mapping with the new username
|
||||||
|
userIds().put(newUser.getUserName().toLowerCase(), newUser.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean filterReply(Update update) {
|
||||||
|
return replies.stream()
|
||||||
|
.filter(reply -> reply.isOkFor(update))
|
||||||
|
.map(reply -> {
|
||||||
|
reply.actOn(update);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.reduce(true, Boolean::logicalAnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean checkMessageFlags(Trio<Update, Ability, String[]> trio) {
|
||||||
|
Ability ability = trio.b();
|
||||||
|
Update update = trio.a();
|
||||||
|
|
||||||
|
// The following variable is required to avoid bug #JDK-8044546
|
||||||
|
BiFunction<Boolean, Predicate<Update>, Boolean> flagAnd = (flag, nextFlag) -> flag && nextFlag.test(update);
|
||||||
|
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("_", "\\_");
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package org.telegram.abilitybots.api.db;
|
package org.telegram.abilitybots.api.db;
|
||||||
|
|
||||||
import org.telegram.abilitybots.api.bot.AbilityBot;
|
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
@ -12,7 +12,7 @@ import java.util.Set;
|
|||||||
* This interface represents the high-level methods exposed to the user when handling an {@link Update}.
|
* This interface represents the high-level methods exposed to the user when handling an {@link Update}.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* <p><code>Ability.builder().action(ctx -> {db.getSet(USERS); doSomething();})* </code></p>
|
* <p><code>Ability.builder().action(ctx -> {db.getSet(USERS); doSomething();})* </code></p>
|
||||||
* {@link AbilityBot} contains a handle on the <code>db</code> that the user can use inside his declared abilities.
|
* {@link BaseAbilityBot} contains a handle on the <code>db</code> that the user can use inside his declared abilities.
|
||||||
*
|
*
|
||||||
* @author Abbas Abou Daya
|
* @author Abbas Abou Daya
|
||||||
*/
|
*/
|
||||||
|
@ -21,7 +21,7 @@ import static java.util.Objects.isNull;
|
|||||||
import static java.util.stream.Collectors.toMap;
|
import static java.util.stream.Collectors.toMap;
|
||||||
import static java.util.stream.StreamSupport.stream;
|
import static java.util.stream.StreamSupport.stream;
|
||||||
import static org.mapdb.Serializer.JAVA;
|
import static org.mapdb.Serializer.JAVA;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USERS;
|
import static org.telegram.abilitybots.api.bot.BaseAbilityBot.USERS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link DBContext} that relies on a {@link DB}.
|
* An implementation of {@link DBContext} that relies on a {@link DB}.
|
||||||
|
@ -13,15 +13,15 @@ import static com.google.common.collect.Maps.newHashMap;
|
|||||||
import static com.google.common.collect.Sets.newHashSet;
|
import static com.google.common.collect.Sets.newHashSet;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USERS;
|
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USER_ID;
|
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR;
|
import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR;
|
||||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.USER;
|
import static org.telegram.abilitybots.api.bot.AbilityBotTest.USER;
|
||||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||||
|
|
||||||
public class MapDBContextTest {
|
public class MapDBContextTest {
|
||||||
|
private static final String USERS = "USERS";
|
||||||
|
private static final String USER_ID = "USER_ID";
|
||||||
private static final String TEST = "TEST";
|
private static final String TEST = "TEST";
|
||||||
|
|
||||||
private DBContext db;
|
private DBContext db;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -777,7 +777,7 @@ public abstract class DefaultAbsSender extends AbsSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getBaseUrl() {
|
public String getBaseUrl() {
|
||||||
return options.getBaseUrl() + getBotToken() + "/";
|
return options.getBaseUrl() + getBotToken() + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,19 +23,11 @@ public abstract class TelegramLongPollingBot extends DefaultAbsSender implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearWebhook() throws TelegramApiRequestException {
|
public void clearWebhook() throws TelegramApiRequestException {
|
||||||
try {
|
WebhookUtils.clearWebhook(this);
|
||||||
boolean result = execute(new DeleteWebhook());
|
|
||||||
if (!result) {
|
|
||||||
throw new TelegramApiRequestException("Error removing old webhook");
|
|
||||||
}
|
|
||||||
} catch (TelegramApiException e) {
|
|
||||||
throw new TelegramApiRequestException("Error removing old webhook", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClosing() {
|
public void onClosing() {
|
||||||
exe.shutdown();
|
exe.shutdown();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
@ -30,54 +30,16 @@ import java.nio.charset.StandardCharsets;
|
|||||||
* @date 14 of January of 2016
|
* @date 14 of January of 2016
|
||||||
*/
|
*/
|
||||||
public abstract class TelegramWebhookBot extends DefaultAbsSender implements WebhookBot {
|
public abstract class TelegramWebhookBot extends DefaultAbsSender implements WebhookBot {
|
||||||
private final DefaultBotOptions botOptions;
|
public TelegramWebhookBot() {
|
||||||
|
this(ApiContext.getInstance(DefaultBotOptions.class));
|
||||||
|
}
|
||||||
|
|
||||||
public TelegramWebhookBot() {
|
public TelegramWebhookBot(DefaultBotOptions options) {
|
||||||
this(ApiContext.getInstance(DefaultBotOptions.class));
|
super(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TelegramWebhookBot(DefaultBotOptions options) {
|
@Override
|
||||||
super(options);
|
public void setWebhook(String url, String publicCertificatePath) throws TelegramApiRequestException {
|
||||||
this.botOptions = options;
|
WebhookUtils.setWebhook(this, url, publicCertificatePath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public void setWebhook(String url, String publicCertificatePath) throws TelegramApiRequestException {
|
|
||||||
try (CloseableHttpClient httpclient = TelegramHttpClientBuilder.build(getOptions())) {
|
|
||||||
String requestUrl = getBaseUrl() + SetWebhook.PATH;
|
|
||||||
|
|
||||||
HttpPost httppost = new HttpPost(requestUrl);
|
|
||||||
httppost.setConfig(botOptions.getRequestConfig());
|
|
||||||
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
|
|
||||||
builder.addTextBody(SetWebhook.URL_FIELD, url);
|
|
||||||
if (botOptions.getMaxWebhookConnections() != null) {
|
|
||||||
builder.addTextBody(SetWebhook.MAXCONNECTIONS_FIELD, botOptions.getMaxWebhookConnections().toString());
|
|
||||||
}
|
|
||||||
if (botOptions.getAllowedUpdates() != null) {
|
|
||||||
builder.addTextBody(SetWebhook.ALLOWEDUPDATES_FIELD, new JSONArray(botOptions.getMaxWebhookConnections()).toString());
|
|
||||||
}
|
|
||||||
if (publicCertificatePath != null) {
|
|
||||||
File certificate = new File(publicCertificatePath);
|
|
||||||
if (certificate.exists()) {
|
|
||||||
builder.addBinaryBody(SetWebhook.CERTIFICATE_FIELD, certificate, ContentType.TEXT_PLAIN, certificate.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HttpEntity multipart = builder.build();
|
|
||||||
httppost.setEntity(multipart);
|
|
||||||
try (CloseableHttpResponse response = httpclient.execute(httppost)) {
|
|
||||||
HttpEntity ht = response.getEntity();
|
|
||||||
BufferedHttpEntity buf = new BufferedHttpEntity(ht);
|
|
||||||
String responseContent = EntityUtils.toString(buf, StandardCharsets.UTF_8);
|
|
||||||
JSONObject jsonObject = new JSONObject(responseContent);
|
|
||||||
if (!jsonObject.getBoolean(ApiConstants.RESPONSE_FIELD_OK)) {
|
|
||||||
throw new TelegramApiRequestException("Error setting webhook", jsonObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new TelegramApiRequestException("Error deserializing setWebhook method response", e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new TelegramApiRequestException("Error executing setWebook method", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,82 @@
|
|||||||
|
package org.telegram.telegrambots.util;
|
||||||
|
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.entity.BufferedHttpEntity;
|
||||||
|
import org.apache.http.entity.ContentType;
|
||||||
|
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.telegram.telegrambots.ApiConstants;
|
||||||
|
import org.telegram.telegrambots.api.methods.updates.DeleteWebhook;
|
||||||
|
import org.telegram.telegrambots.api.methods.updates.SetWebhook;
|
||||||
|
import org.telegram.telegrambots.bots.DefaultAbsSender;
|
||||||
|
import org.telegram.telegrambots.bots.DefaultBotOptions;
|
||||||
|
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||||
|
import org.telegram.telegrambots.exceptions.TelegramApiRequestException;
|
||||||
|
import org.telegram.telegrambots.facilities.TelegramHttpClientBuilder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public final class WebhookUtils {
|
||||||
|
private WebhookUtils() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setWebhook(DefaultAbsSender bot, String url, String publicCertificatePath) throws TelegramApiRequestException {
|
||||||
|
DefaultBotOptions botOptions = bot.getOptions();
|
||||||
|
|
||||||
|
try (CloseableHttpClient httpclient = TelegramHttpClientBuilder.build(botOptions)) {
|
||||||
|
String requestUrl = bot.getBaseUrl() + SetWebhook.PATH;
|
||||||
|
|
||||||
|
HttpPost httppost = new HttpPost(requestUrl);
|
||||||
|
httppost.setConfig(botOptions.getRequestConfig());
|
||||||
|
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
|
||||||
|
builder.addTextBody(SetWebhook.URL_FIELD, url);
|
||||||
|
if (botOptions.getMaxWebhookConnections() != null) {
|
||||||
|
builder.addTextBody(SetWebhook.MAXCONNECTIONS_FIELD, botOptions.getMaxWebhookConnections().toString());
|
||||||
|
}
|
||||||
|
if (botOptions.getAllowedUpdates() != null) {
|
||||||
|
builder.addTextBody(SetWebhook.ALLOWEDUPDATES_FIELD, new JSONArray(botOptions.getMaxWebhookConnections()).toString());
|
||||||
|
}
|
||||||
|
if (publicCertificatePath != null) {
|
||||||
|
File certificate = new File(publicCertificatePath);
|
||||||
|
if (certificate.exists()) {
|
||||||
|
builder.addBinaryBody(SetWebhook.CERTIFICATE_FIELD, certificate, ContentType.TEXT_PLAIN, certificate.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpEntity multipart = builder.build();
|
||||||
|
httppost.setEntity(multipart);
|
||||||
|
try (CloseableHttpResponse response = httpclient.execute(httppost)) {
|
||||||
|
HttpEntity ht = response.getEntity();
|
||||||
|
BufferedHttpEntity buf = new BufferedHttpEntity(ht);
|
||||||
|
String responseContent = EntityUtils.toString(buf, StandardCharsets.UTF_8);
|
||||||
|
JSONObject jsonObject = new JSONObject(responseContent);
|
||||||
|
if (!jsonObject.getBoolean(ApiConstants.RESPONSE_FIELD_OK)) {
|
||||||
|
throw new TelegramApiRequestException("Error setting webhook", jsonObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new TelegramApiRequestException("Error deserializing setWebhook method response", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new TelegramApiRequestException("Error executing setWebook method", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearWebhook(DefaultAbsSender bot) throws TelegramApiRequestException {
|
||||||
|
try {
|
||||||
|
boolean result = bot.execute(new DeleteWebhook());
|
||||||
|
if (!result) {
|
||||||
|
throw new TelegramApiRequestException("Error removing old webhook");
|
||||||
|
}
|
||||||
|
} catch (TelegramApiException e) {
|
||||||
|
throw new TelegramApiRequestException("Error removing old webhook", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user