715 lines
27 KiB
Java
715 lines
27 KiB
Java
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 org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.telegram.abilitybots.api.db.DBContext;
|
|
import org.telegram.abilitybots.api.objects.Ability;
|
|
import org.telegram.abilitybots.api.objects.Locality;
|
|
import org.telegram.abilitybots.api.objects.MessageContext;
|
|
import org.telegram.abilitybots.api.objects.Privacy;
|
|
import org.telegram.abilitybots.api.objects.Reply;
|
|
import org.telegram.abilitybots.api.objects.ReplyCollection;
|
|
import org.telegram.abilitybots.api.objects.Stats;
|
|
import org.telegram.abilitybots.api.sender.DefaultSender;
|
|
import org.telegram.abilitybots.api.sender.MessageSender;
|
|
import org.telegram.abilitybots.api.sender.SilentSender;
|
|
import org.telegram.abilitybots.api.toggle.AbilityToggle;
|
|
import org.telegram.abilitybots.api.util.AbilityExtension;
|
|
import org.telegram.abilitybots.api.util.AbilityUtils;
|
|
import org.telegram.abilitybots.api.util.Pair;
|
|
import org.telegram.abilitybots.api.util.Trio;
|
|
import org.telegram.telegrambots.bots.DefaultAbsSender;
|
|
import org.telegram.telegrambots.bots.DefaultBotOptions;
|
|
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
|
|
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.api.objects.chatmember.ChatMemberAdministrator;
|
|
import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMemberOwner;
|
|
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.function.BiFunction;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
import static com.google.common.collect.Sets.difference;
|
|
import static java.lang.String.format;
|
|
import static java.time.ZonedDateTime.now;
|
|
import static java.util.Arrays.stream;
|
|
import static java.util.Comparator.comparingInt;
|
|
import static java.util.Objects.isNull;
|
|
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.toSet;
|
|
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.Privacy.ADMIN;
|
|
import static org.telegram.abilitybots.api.objects.Privacy.CREATOR;
|
|
import static org.telegram.abilitybots.api.objects.Privacy.GROUP_ADMIN;
|
|
import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
|
|
import static org.telegram.abilitybots.api.objects.Stats.createStats;
|
|
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.AbilityUtils.EMPTY_USER;
|
|
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.getUser;
|
|
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;
|
|
|
|
/**
|
|
* 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 DefaultAbsSender}.
|
|
* <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
|
|
*/
|
|
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused", "ConstantConditions"})
|
|
public abstract class BaseAbilityBot extends DefaultAbsSender implements AbilityExtension {
|
|
private static final Logger log = LoggerFactory.getLogger(BaseAbilityBot.class);
|
|
|
|
protected static final String DEFAULT = "default";
|
|
// DB objects
|
|
public static final String ADMINS = "ADMINS";
|
|
public static final String USERS = "USERS";
|
|
public static final String USER_ID = "USER_ID";
|
|
public static final String BLACKLIST = "BLACKLIST";
|
|
public static final String STATS = "ABILITYBOT_STATS";
|
|
|
|
// DB and sender
|
|
protected final DBContext db;
|
|
protected MessageSender sender;
|
|
protected SilentSender silent;
|
|
|
|
// Ability toggle
|
|
private final AbilityToggle toggle;
|
|
|
|
// Bot username
|
|
private final String botUsername;
|
|
|
|
// Ability registry
|
|
private final List<AbilityExtension> extensions = new ArrayList<>();
|
|
private Map<String, Ability> abilities;
|
|
private Map<String, Stats> stats;
|
|
|
|
// Reply registry
|
|
private List<Reply> replies;
|
|
|
|
public abstract long creatorId();
|
|
|
|
protected BaseAbilityBot(String botToken, String botUsername, DBContext db, AbilityToggle toggle, DefaultBotOptions botOptions) {
|
|
super(botOptions, botToken);
|
|
|
|
this.botUsername = botUsername;
|
|
this.db = db;
|
|
this.toggle = toggle;
|
|
this.sender = new DefaultSender(this);
|
|
silent = new SilentSender(sender);
|
|
}
|
|
|
|
public void onRegister() {
|
|
registerAbilities();
|
|
initStats();
|
|
}
|
|
|
|
/**
|
|
* @return the database of this bot
|
|
*/
|
|
public DBContext db() {
|
|
return db;
|
|
}
|
|
|
|
/**
|
|
* @return the message sender for this bot
|
|
*/
|
|
public MessageSender sender() {
|
|
return sender;
|
|
}
|
|
|
|
/**
|
|
* @return the silent sender for this bot
|
|
*/
|
|
public SilentSender silent() {
|
|
return silent;
|
|
}
|
|
|
|
/**
|
|
* @return the map of <ID,User>
|
|
*/
|
|
public Map<Long, User> users() {
|
|
return db.getMap(USERS);
|
|
}
|
|
|
|
/**
|
|
* @return the map of <Username,ID>
|
|
*/
|
|
public Map<String, Long> userIds() {
|
|
return db.getMap(USER_ID);
|
|
}
|
|
|
|
/**
|
|
* @return a blacklist containing all the IDs of the banned users
|
|
*/
|
|
public Set<Long> blacklist() {
|
|
return db.getSet(BLACKLIST);
|
|
}
|
|
|
|
/**
|
|
* @return an admin set of all the IDs of bot administrators
|
|
*/
|
|
public Set<Long> admins() {
|
|
return db.getSet(ADMINS);
|
|
}
|
|
|
|
/**
|
|
* @return a mapping of ability and reply names to their corresponding statistics
|
|
*/
|
|
public Map<String, Stats> stats() {
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
log.info(format("[%s] New update [%s] received at %s", botUsername, update.getUpdateId(), now()));
|
|
log.info(update.toString());
|
|
long millisStarted = System.currentTimeMillis();
|
|
|
|
Stream.of(update)
|
|
.filter(this::checkGlobalFlags)
|
|
.filter(this::checkBlacklist)
|
|
.map(this::addUser)
|
|
.filter(this::filterReply)
|
|
.filter(this::hasUser)
|
|
.map(this::getAbility)
|
|
.filter(this::validateAbility)
|
|
.filter(this::checkPrivacy)
|
|
.filter(this::checkLocality)
|
|
.filter(this::checkInput)
|
|
.filter(this::checkMessageFlags)
|
|
.map(this::getContext)
|
|
.map(this::consumeUpdate)
|
|
.map(this::updateStats)
|
|
.forEach(this::postConsumption);
|
|
|
|
// Commit to DB now after all the actions have been dealt
|
|
db.commit();
|
|
|
|
long processingTime = System.currentTimeMillis() - millisStarted;
|
|
log.info(format("[%s] Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", botUsername, update.getUpdateId(), now(), processingTime));
|
|
}
|
|
|
|
public String getBotUsername() {
|
|
return botUsername;
|
|
}
|
|
|
|
public Privacy getPrivacy(Update update, long id) {
|
|
return isCreator(id) ?
|
|
CREATOR : isAdmin(id) ?
|
|
ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ?
|
|
GROUP_ADMIN : PUBLIC;
|
|
}
|
|
|
|
public boolean isGroupAdmin(Update update, long id) {
|
|
return isGroupAdmin(getChatId(update), id);
|
|
}
|
|
|
|
public boolean isGroupAdmin(long chatId, long id) {
|
|
GetChatAdministrators admins = GetChatAdministrators.builder().chatId(chatId).build();
|
|
return silent.execute(admins)
|
|
.orElse(new ArrayList<>())
|
|
.stream()
|
|
.map(member -> {
|
|
final String status = member.getStatus();
|
|
if (status.equals(ChatMemberOwner.STATUS)
|
|
|| status.equals(ChatMemberAdministrator.STATUS)) {
|
|
return member.getUser().getId();
|
|
}
|
|
return 0L;
|
|
})
|
|
.anyMatch(member -> member == id);
|
|
}
|
|
|
|
public boolean isCreator(long id) {
|
|
return id == creatorId();
|
|
}
|
|
|
|
public boolean isAdmin(long id) {
|
|
return admins().contains(id);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
protected String getCommandPrefix() {
|
|
return "/";
|
|
}
|
|
|
|
protected String getCommandRegexSplit() {
|
|
return " ";
|
|
}
|
|
|
|
protected boolean allowContinuousText() {
|
|
return false;
|
|
}
|
|
|
|
protected void addExtension(AbilityExtension extension) {
|
|
this.extensions.add(extension);
|
|
}
|
|
|
|
protected void addExtensions(AbilityExtension... extensions) {
|
|
this.extensions.addAll(Arrays.asList(extensions));
|
|
}
|
|
|
|
protected void addExtensions(Collection<AbilityExtension> extensions) {
|
|
this.extensions.addAll(extensions);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
// Collect all classes that implement AbilityExtension declared in the bot
|
|
extensions.addAll(stream(getClass().getMethods())
|
|
.filter(checkReturnType(AbilityExtension.class))
|
|
.map(returnExtension(this))
|
|
.collect(Collectors.toList()));
|
|
|
|
// Add the bot itself as it is an AbilityExtension
|
|
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
|
|
abilities = Stream.concat(defaultAbsStream,
|
|
extensions.stream()
|
|
.flatMap(ext -> stream(ext.getClass().getMethods())
|
|
.filter(checkReturnType(Ability.class))
|
|
.map(returnAbility(ext))))
|
|
// Abilities are immutable, build it respectively
|
|
.collect(ImmutableMap::<String, Ability>builder,
|
|
(b, a) -> b.put(a.name(), a),
|
|
(b1, b2) -> b1.putAll(b2.build()))
|
|
.build();
|
|
|
|
// Extract all replies from every single extension instance
|
|
Stream<Reply> extensionReplies = extensions.stream()
|
|
.flatMap(ext -> stream(ext.getClass().getMethods())
|
|
.filter(checkReturnType(Reply.class))
|
|
.map(returnReply(ext)))
|
|
.flatMap(Reply::stream);
|
|
|
|
// Extract all replies from extension instances methods, returning ReplyCollection
|
|
Stream<Reply> extensionCollectionReplies = extensions.stream()
|
|
.flatMap(extension -> stream(extension.getClass().getMethods())
|
|
.filter(checkReturnType(ReplyCollection.class))
|
|
.map(returnReplyCollection(extension))
|
|
.flatMap(ReplyCollection::stream));
|
|
|
|
// Replies can be standalone or attached to abilities, fetch those too
|
|
Stream<Reply> abilityReplies = abilities.values().stream()
|
|
.flatMap(ability -> ability.replies().stream())
|
|
.flatMap(Reply::stream);
|
|
|
|
// Now create the replies registry (list)
|
|
replies = Stream.of(abilityReplies, extensionReplies, extensionCollectionReplies)
|
|
.flatMap(replyStream -> replyStream)
|
|
.collect(
|
|
ImmutableList::<Reply>builder,
|
|
Builder::add,
|
|
(b1, b2) -> b1.addAll(b2.build()))
|
|
.build();
|
|
} catch (IllegalStateException e) {
|
|
log.error("Duplicate names found while registering abilities. Make sure that the abilities declared don't clash with the reserved ones.", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private void initStats() {
|
|
Set<String> enabledStats = Stream.concat(
|
|
replies.stream().filter(Reply::statsEnabled).map(Reply::name),
|
|
abilities.entrySet().stream()
|
|
.filter(entry -> entry.getValue().statsEnabled())
|
|
.map(Map.Entry::getKey)).collect(toSet());
|
|
stats = db.getMap(STATS);
|
|
Set<String> toBeRemoved = difference(stats.keySet(), enabledStats);
|
|
toBeRemoved.forEach(stats::remove);
|
|
enabledStats.forEach(abName -> stats.computeIfAbsent(abName,
|
|
name -> createStats(abName, 0)));
|
|
}
|
|
|
|
/**
|
|
* @param clazz the type to be tested
|
|
* @return a predicate testing the return type of the method corresponding to the class parameter
|
|
*/
|
|
private static Predicate<Method> checkReturnType(Class<?> clazz) {
|
|
return method -> clazz.isAssignableFrom(method.getReturnType());
|
|
}
|
|
|
|
/**
|
|
* Invokes the method and retrieves its return {@link Reply}.
|
|
*
|
|
* @param obj a bot or extension that this method is invoked with
|
|
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
|
*/
|
|
private Function<? super Method, AbilityExtension> returnExtension(Object obj) {
|
|
return method -> {
|
|
try {
|
|
return (AbilityExtension) method.invoke(obj);
|
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
log.error("Could not add ability extension", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Invokes the method and retrieves its return {@link Ability}.
|
|
*
|
|
* @param obj a bot or extension that this method is invoked with
|
|
* @return a {@link Function} which returns the {@link Ability} returned by the given method
|
|
*/
|
|
private static Function<? super Method, Ability> returnAbility(Object obj) {
|
|
return method -> {
|
|
try {
|
|
return (Ability) method.invoke(obj);
|
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
log.error("Could not add ability", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Invokes the method and retrieves its return {@link Reply}.
|
|
*
|
|
* @param obj a bot or extension that this method is invoked with
|
|
* @return a {@link Function} which returns the {@link Reply} returned by the given method
|
|
*/
|
|
private static Function<? super Method, Reply> returnReply(Object obj) {
|
|
return method -> {
|
|
try {
|
|
return (Reply) method.invoke(obj);
|
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
log.error("Could not add reply", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Invokes the method and retrieves its return {@link ReplyCollection}.
|
|
*
|
|
* @param obj a bot or extension that this method is invoked with
|
|
* @return a {@link Function} which returns the {@link ReplyCollection} returned by the given method
|
|
*/
|
|
private static Function<? super Method, ReplyCollection> returnReplyCollection(Object obj) {
|
|
return method -> {
|
|
try {
|
|
return (ReplyCollection) method.invoke(obj);
|
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
log.error("Could not add Reply Collection", e);
|
|
throw new RuntimeException(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> updateStats(Pair<MessageContext, Ability> pair) {
|
|
Ability ab = pair.b();
|
|
if (ab.statsEnabled()) {
|
|
updateStats(pair.b().name());
|
|
}
|
|
return pair;
|
|
}
|
|
|
|
private void updateReplyStats(Reply reply) {
|
|
if (reply.statsEnabled()) {
|
|
updateStats(reply.name());
|
|
}
|
|
}
|
|
|
|
void updateStats(String name) {
|
|
Stats statsObj = stats.get(name);
|
|
statsObj.hit();
|
|
stats.put(name, statsObj);
|
|
}
|
|
|
|
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), this, trio.c()), trio.b());
|
|
}
|
|
|
|
boolean checkBlacklist(Update update) {
|
|
User user = getUser(update);
|
|
if (isNull(user)) {
|
|
return true;
|
|
}
|
|
|
|
long id = user.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;
|
|
long 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;
|
|
}
|
|
|
|
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[]{});
|
|
|
|
Ability ability;
|
|
String[] tokens;
|
|
if (allowContinuousText()) {
|
|
String abName = abilities.keySet().stream()
|
|
.filter(name -> msg.getText().startsWith(format("%s%s", getCommandPrefix(), name)))
|
|
.max(comparingInt(String::length))
|
|
.orElse(DEFAULT);
|
|
tokens = msg.getText()
|
|
.replaceFirst(getCommandPrefix() + abName, "")
|
|
.split(getCommandRegexSplit());
|
|
ability = abilities.get(abName);
|
|
} else {
|
|
tokens = msg.getText().split(getCommandRegexSplit());
|
|
if (tokens[0].startsWith(getCommandPrefix())) {
|
|
String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase();
|
|
ability = abilities.get(abilityToken);
|
|
tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
|
|
} else {
|
|
ability = abilities.get(DEFAULT);
|
|
}
|
|
}
|
|
return Trio.of(update, ability, tokens);
|
|
}
|
|
|
|
private String stripBotUsername(String token) {
|
|
return compile(format("@%s", botUsername), CASE_INSENSITIVE)
|
|
.matcher(token)
|
|
.replaceAll("");
|
|
}
|
|
|
|
Update addUser(Update update) {
|
|
User endUser = AbilityUtils.getUser(update);
|
|
if (endUser.equals(EMPTY_USER)) {
|
|
// Can't add an empty user, return the update as is
|
|
return 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;
|
|
});
|
|
|
|
return update;
|
|
}
|
|
|
|
private boolean hasUser(Update update) {
|
|
// Valid updates without users should return an empty user
|
|
// Updates that are not recognized by the getUser method will throw an exception
|
|
return !AbilityUtils.getUser(update).equals(EMPTY_USER);
|
|
}
|
|
|
|
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 -> runSilently(() -> reply.isOkFor(update), reply.name()))
|
|
.map(reply -> runSilently(() -> {
|
|
reply.actOn(this, update);
|
|
updateReplyStats(reply);
|
|
return false;
|
|
}, reply.name()))
|
|
.reduce(true, Boolean::logicalAnd);
|
|
}
|
|
|
|
boolean runSilently(Callable<Boolean> callable, String name) {
|
|
try {
|
|
return callable.call();
|
|
} catch(Exception ex) {
|
|
String msg = format("Reply [%s] failed to check for conditions. " +
|
|
"Make sure you're safeguarding against all possible updates.", name);
|
|
if (log.isDebugEnabled()) {
|
|
log.error(msg, ex);
|
|
} else {
|
|
log.error(msg);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|