Expose abilities and replies, add report command and reformat /commands, closes #436

This commit is contained in:
Abbas Abou Daya 2018-05-23 18:59:03 -04:00
parent b0b2504e01
commit 889fd46834
5 changed files with 171 additions and 80 deletions

View File

@ -1,6 +1,12 @@
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;
@ -27,22 +33,24 @@ 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.function.Function.identity;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.compile;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
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.*;
@ -102,6 +110,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
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;
@ -172,6 +181,20 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
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>
@ -260,7 +283,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
* 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
* @param ctx the message context with the originating user
* @return the id of the user
*/
protected int getUserIdSendError(String username, MessageContext ctx) {
@ -290,9 +313,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
*/
public Ability reportCommands() {
return builder()
.name(COMMANDS)
.name(REPORT)
.locality(ALL)
.privacy(PUBLIC)
.privacy(CREATOR)
.input(0)
.action(ctx -> {
String commands = abilities.entrySet().stream()
@ -311,6 +334,63 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.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>
@ -507,22 +587,17 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
return builder()
.name(CLAIM)
.locality(ALL)
.privacy(PUBLIC)
.privacy(CREATOR)
.input(0)
.action(ctx -> {
if (ctx.user().getId() == creatorId()) {
Set<Integer> admins = admins();
int id = creatorId();
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);
}
} else {
// This is not a joke
abilities.get(BAN).action().accept(newContext(ctx.update(), ctx.user(), ctx.chatId(), ctx.user().getUserName()));
if (admins.contains(id))
send(ABILITY_CLAIM_FAIL, ctx);
else {
admins.add(id);
send(ABILITY_CLAIM_SUCCESS, ctx);
}
})
.post(commitTo(db))
@ -552,7 +627,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
abilities = stream(this.getClass().getMethods())
.filter(method -> method.getReturnType().equals(Ability.class))
.map(this::returnAbility)
.collect(toMap(ability -> ability.name().toLowerCase(), identity()));
.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))
@ -561,7 +639,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Stream<Reply> abilityReplies = abilities.values().stream()
.flatMap(ability -> ability.replies().stream());
replies = Stream.concat(methodReplies, abilityReplies).collect(toList());
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);
@ -661,7 +743,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Privacy privacy;
int id = user.getId();
privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ? GROUP_ADMIN : PUBLIC;
privacy = getPrivacy(update, id);
boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
@ -674,6 +756,14 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
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));

View File

@ -1,4 +1,4 @@
ability.commands.notFound=No public commands found.
ability.commands.notFound=No available commands found.
ability.recover.success=I have successfully recovered.
ability.recover.fail=Oops, something went wrong during recovery.

View File

@ -42,23 +42,23 @@ public class AbilityBotI18nTest {
}
@Test
public void missingPublicCommandsLocalizedCorrectly1() {
public void missingPublicCommandsLocalizedInEnglishByDefault() {
MessageContext context = mockContext(NO_LANGUAGE_USER);
bot.reportCommands().action().accept(context);
verify(silent, times(1))
.send("No public commands found.", NO_LANGUAGE_USER.getId());
.send("No available commands found.", NO_LANGUAGE_USER.getId());
}
@Test
public void missingPublicCommandsLocalizedCorrectly2() {
public void missingPublicCommandsLocalizedInItalian() {
MessageContext context = mockContext(ITALIAN_USER);
bot.reportCommands().action().accept(context);
verify(silent, times(1))
.send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId());
.send("Non sono presenti comandi disponibile.", ITALIAN_USER.getId());
}
@After

View File

@ -198,8 +198,7 @@ public class AbilityBotTest {
@NotNull
private MessageContext defaultContext() {
MessageContext context = mockContext(CREATOR, GROUP_ID, USER.getUserName());
return context;
return mockContext(CREATOR, GROUP_ID, USER.getUserName());
}
@Test
@ -232,22 +231,6 @@ public class AbilityBotTest {
assertEquals("Creator was not properly added to the super admins set", expected, actual);
}
@Test
public void userGetsBannedIfClaimsBot() {
addUsers(USER);
MessageContext context = mockContext(USER, GROUP_ID);
bot.claimCreator().action().accept(context);
Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(USER.getId());
assertEquals("Could not find user on the blacklist", expected, actual);
actual = bot.admins();
expected = emptySet();
assertEquals("Admins set is not empty", expected, actual);
}
@Test
public void bannedCreatorPassesBlacklistCheck() {
bot.blacklist().add(CREATOR.getId());
@ -265,7 +248,6 @@ public class AbilityBotTest {
public void canAddUser() {
Update update = mock(Update.class);
Message message = mock(Message.class);
User user = mock(User.class);
mockAlternateUser(update, message, USER);
@ -282,7 +264,6 @@ public class AbilityBotTest {
addUsers(USER);
Update update = mock(Update.class);
Message message = mock(Message.class);
User user = mock(User.class);
String newUsername = USER.getUserName() + "-test";
String newFirstName = USER.getFirstName() + "-test";
@ -306,8 +287,8 @@ public class AbilityBotTest {
Ability validAbility = getDefaultBuilder().build();
Trio<Update, Ability, String[]> validPair = Trio.of(null, validAbility, null);
assertEquals("Bot can't validate ability properly", false, bot.validateAbility(invalidPair));
assertEquals("Bot can't validate ability properly", true, bot.validateAbility(validPair));
assertFalse("Bot can't validate ability properly", bot.validateAbility(invalidPair));
assertTrue("Bot can't validate ability properly", bot.validateAbility(validPair));
}
@Test
@ -322,15 +303,15 @@ public class AbilityBotTest {
Trio<Update, Ability, String[]> trioOneArg = Trio.of(update, abilityWithOneInput, TEXT);
Trio<Update, Ability, String[]> trioZeroArg = Trio.of(update, abilityWithZeroInput, TEXT);
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioOneArg));
assertTrue("Unexpected result when applying token filter", bot.checkInput(trioOneArg));
trioOneArg = Trio.of(update, abilityWithOneInput, addAll(TEXT, TEXT));
assertEquals("Unexpected result when applying token filter", false, bot.checkInput(trioOneArg));
assertFalse("Unexpected result when applying token filter", bot.checkInput(trioOneArg));
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg));
assertTrue("Unexpected result when applying token filter", bot.checkInput(trioZeroArg));
trioZeroArg = Trio.of(update, abilityWithZeroInput, EMPTY_ARRAY);
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg));
assertTrue("Unexpected result when applying token filter", bot.checkInput(trioZeroArg));
}
@Test
@ -350,10 +331,10 @@ public class AbilityBotTest {
mockUser(update, message, user);
assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(publicTrio));
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(groupAdminTrio));
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(adminTrio));
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio));
assertTrue("Unexpected result when checking for privacy", bot.checkPrivacy(publicTrio));
assertFalse("Unexpected result when checking for privacy", bot.checkPrivacy(groupAdminTrio));
assertFalse("Unexpected result when checking for privacy", bot.checkPrivacy(adminTrio));
assertFalse("Unexpected result when checking for privacy", bot.checkPrivacy(creatorTrio));
}
@Test
@ -374,7 +355,7 @@ public class AbilityBotTest {
when(silent.execute(any(GetChatAdministrators.class))).thenReturn(Optional.of(newArrayList(member)));
assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(groupAdminTrio));
assertTrue("Unexpected result when checking for privacy", bot.checkPrivacy(groupAdminTrio));
}
@Test
@ -391,7 +372,7 @@ public class AbilityBotTest {
when(silent.execute(any(GetChatAdministrators.class))).thenReturn(empty());
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(groupAdminTrio));
assertFalse("Unexpected result when checking for privacy", bot.checkPrivacy(groupAdminTrio));
}
@Test
@ -406,7 +387,7 @@ public class AbilityBotTest {
bot.admins().add(USER.getId());
mockUser(update, message, user);
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio));
assertFalse("Unexpected result when checking for privacy", bot.checkPrivacy(creatorTrio));
}
@Test
@ -425,9 +406,9 @@ public class AbilityBotTest {
mockUser(update, message, user);
when(message.isUserMessage()).thenReturn(true);
assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(publicTrio));
assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(userTrio));
assertEquals("Unexpected result when checking for locality", false, bot.checkLocality(groupTrio));
assertTrue("Unexpected result when checking for locality", bot.checkLocality(publicTrio));
assertTrue("Unexpected result when checking for locality", bot.checkLocality(userTrio));
assertFalse("Unexpected result when checking for locality", bot.checkLocality(groupTrio));
}
@Test
@ -449,7 +430,7 @@ public class AbilityBotTest {
@Test
public void defaultGlobalFlagIsTrue() {
Update update = mock(Update.class);
assertEquals("Unexpected result when checking for the default global flags", true, bot.checkGlobalFlags(update));
assertTrue("Unexpected result when checking for the default global flags", bot.checkGlobalFlags(update));
}
@Test(expected = ArithmeticException.class)
@ -536,8 +517,8 @@ public class AbilityBotTest {
Trio<Update, Ability, String[]> docTrio = Trio.of(update, documentAbility, TEXT);
Trio<Update, Ability, String[]> textTrio = Trio.of(update, textAbility, TEXT);
assertEquals("Unexpected result when checking for message flags", false, bot.checkMessageFlags(docTrio));
assertEquals("Unexpected result when checking for message flags", true, bot.checkMessageFlags(textTrio));
assertFalse("Unexpected result when checking for message flags", bot.checkMessageFlags(docTrio));
assertTrue("Unexpected result when checking for message flags", bot.checkMessageFlags(textTrio));
}
@Test
@ -568,6 +549,39 @@ public class AbilityBotTest {
return newContext(update, user, groupId, args);
}
@Test
public void canPrintCommandsBasedOnPrivacy() {
Update update = mock(Update.class);
Message message = mock(Message.class);
when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message);
when(message.hasText()).thenReturn(true);
MessageContext creatorCtx = newContext(update, CREATOR, GROUP_ID);
bot.commands().action().accept(creatorCtx);
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report";
verify(silent, times(1)).send(expected, GROUP_ID);
}
@Test
public void printsOnlyPublicCommandsForNormalUser() {
Update update = mock(Update.class);
Message message = mock(Message.class);
when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message);
when(message.hasText()).thenReturn(true);
MessageContext userCtx = newContext(update, USER, GROUP_ID);
bot.commands().action().accept(userCtx);
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test";
verify(silent, times(1)).send(expected, GROUP_ID);
}
@After
public void tearDown() throws IOException {
db.clear();
@ -640,17 +654,4 @@ public class AbilityBotTest {
writer.close();
return backupFile;
}
public static User newUser(Integer id, String firstname, String lastname, String username, String languageCode) {
User user = mock(User.class);
when(user.getBot()).thenReturn(false);
when(user.getFirstName()).thenReturn(firstname);
when(user.getId()).thenReturn(id);
when(user.getLastName()).thenReturn(lastname);
when(user.getUserName()).thenReturn(username);
when(user.getLanguageCode()).thenReturn(languageCode);
return user;
}
}

View File

@ -1 +1 @@
ability.commands.notFound=Non sono presenti comandi pubblici.
ability.commands.notFound=Non sono presenti comandi disponibile.