Merge pull request #545 from romangraef/dev

Extensions for AbilityBots
This commit is contained in:
Ruben Bermudez 2019-01-27 22:54:54 +00:00 committed by GitHub
commit 18412d9c98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 31 deletions

View File

@ -12,20 +12,21 @@ 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.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.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.methods.GetFile;
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
import org.telegram.telegrambots.meta.api.methods.send.SendDocument;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.User;
import org.telegram.telegrambots.bots.DefaultAbsSender;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.meta.logging.BotLogger;
import org.telegram.telegrambots.meta.api.objects.Update;
import java.io.File;
import java.io.FileNotFoundException;
@ -36,7 +37,9 @@ import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.base.Strings.isNullOrEmpty;
@ -93,7 +96,7 @@ import static org.telegram.abilitybots.api.util.AbilityUtils.*;
* @author Abbas Abou Daya
*/
@SuppressWarnings({"ConfusingArgumentToVarargsMethod", "UnusedReturnValue", "WeakerAccess", "unused", "ConstantConditions"})
public abstract class BaseAbilityBot extends DefaultAbsSender {
public abstract class BaseAbilityBot extends DefaultAbsSender implements AbilityExtension {
private static final String TAG = BaseAbilityBot.class.getSimpleName();
// DB objects
@ -611,22 +614,38 @@ public abstract class BaseAbilityBot extends DefaultAbsSender {
*/
private void registerAbilities() {
try {
abilities = stream(this.getClass().getMethods())
.filter(method -> method.getReturnType().equals(Ability.class))
.map(this::returnAbility)
// Collect all classes that implement AbilityExtension declared in the bot
List<AbilityExtension> extensions = 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);
// Extract all abilities from every single extension instance
abilities = 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();
Stream<Reply> methodReplies = stream(this.getClass().getMethods())
.filter(method -> method.getReturnType().equals(Reply.class))
.map(this::returnReply);
// 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)));
// Replies can be standalone or attached to abilities, fetch those too
Stream<Reply> abilityReplies = abilities.values().stream()
.flatMap(ability -> ability.replies().stream());
replies = Stream.concat(methodReplies, abilityReplies).collect(
// Now create the replies registry (list)
replies = Stream.concat(abilityReplies, extensionReplies).collect(
ImmutableList::<Reply>builder,
Builder::add,
(b1, b2) -> b1.addAll(b2.build()))
@ -635,37 +654,65 @@ public abstract class BaseAbilityBot extends DefaultAbsSender {
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);
}
}
/**
* @param clazz the type to be tested
* @return a predicate testing the return type of the method corresponding to the class parameter
*/
private Predicate<Method> checkReturnType(Class<?> clazz) {
return method -> clazz.isAssignableFrom(method.getReturnType());
}
/**
* Invokes the method and retrieves its return {@link Reply}.
*
* @param obj an bot or extension that this method is invoked with
* @return a {@link Function} which returns the {@link Reply} returned by the given method
*/
private Function<? super Method, AbilityExtension> returnExtension(Object obj) {
return method -> {
try {
return (AbilityExtension) method.invoke(obj);
} catch (IllegalAccessException | InvocationTargetException e) {
BotLogger.error("Could not add ability extension", TAG, 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
* @param obj an bot or extension that this method is invoked with
* @return a {@link Function} which returns the {@link Ability} returned by the given method
*/
private 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);
}
private Function<? super Method, Ability> returnAbility(Object obj) {
return method -> {
try {
return (Ability) method.invoke(obj);
} catch (IllegalAccessException | InvocationTargetException e) {
BotLogger.error("Could not add ability", TAG, e);
throw propagate(e);
}
};
}
/**
* Invokes the method and retrieves its returned Reply.
* Invokes the method and retrieves its return {@link Reply}.
*
* @param method a method that returns a reply
* @return the reply returned by the method
* @param obj an bot or extension that this method is invoked with
* @return a {@link Function} which returns the {@link Reply} returned by the given method
*/
private 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 Function<? super Method, Reply> returnReply(Object obj) {
return method -> {
try {
return (Reply) method.invoke(obj);
} catch (IllegalAccessException | InvocationTargetException e) {
BotLogger.error("Could not add reply", TAG, e);
throw propagate(e);
}
};
}
private void postConsumption(Pair<MessageContext, Ability> pair) {

View File

@ -0,0 +1,7 @@
package org.telegram.abilitybots.api.util;
/**
* An interface to mark a class as an extension. Similar to when a method returns an Ability, it is added to the bot, a method which returns an AbilityExtension will add all Abilities or Replies from this Extension to the bot.
*/
public interface AbilityExtension {
}

View File

@ -0,0 +1,89 @@
package org.telegram.abilitybots.api.bot;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.telegram.abilitybots.api.objects.Ability;
import org.telegram.abilitybots.api.util.AbilityExtension;
import java.io.IOException;
import static junit.framework.TestCase.assertTrue;
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
import static org.telegram.abilitybots.api.objects.Locality.ALL;
import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
public class ExtensionTest {
private ExtensionUsingBot bot;
@Before
public void setUp() {
bot = new ExtensionUsingBot();
}
@Test
public void methodReturningAbilities() {
assertTrue("Failed to find Ability in directly declared in root extension/bot", hasAbilityNamed("direct"));
assertTrue("Failed to find Ability in directly declared in extension returned by method returning the AbilityExtension class", hasAbilityNamed("returningSuperClass0abc"));
assertTrue("Failed to find Ability in directly declared in extension returned by method returning the AbilityExtension subclass", hasAbilityNamed("returningSubClass0abc"));
}
@After
public void tearDown() throws IOException {
bot.db.clear();
bot.db.close();
}
private boolean hasAbilityNamed(String name) {
return bot.abilities().values().stream().map(Ability::name).anyMatch(name::equals);
}
public static class ExtensionUsingBot extends AbilityBot {
public ExtensionUsingBot() {
super("", "", offlineInstance("testing"));
}
@Override
public int creatorId() {
return 0;
}
public AbilityBotExtension methodReturningExtensionSubClass() {
return new AbilityBotExtension("returningSubClass");
}
public AbilityExtension methodReturningExtensionSuperClass() {
return new AbilityBotExtension("returningSuperClass");
}
public Ability methodReturningAbility() {
return Ability.builder()
.name("direct")
.info("Test ability")
.locality(ALL)
.privacy(PUBLIC)
.action(messageContext -> {
})
.build();
}
}
public static class AbilityBotExtension implements AbilityExtension {
private String name;
public AbilityBotExtension(String name) {
this.name = name;
}
public Ability abc() {
return Ability.builder()
.name(name + "0abc")
.info("Test ability")
.locality(ALL)
.privacy(PUBLIC)
.action(ctx -> {
})
.build();
}
}
}