From 889fd4683489135a1be3e427f0c2ce01156a230a Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Wed, 23 May 2018 18:59:03 -0400 Subject: [PATCH] Expose abilities and replies, add report command and reformat /commands, closes #436 --- .../abilitybots/api/bot/AbilityBot.java | 134 +++++++++++++++--- .../src/main/resources/messages.properties | 2 +- .../api/bot/AbilityBotI18nTest.java | 8 +- .../abilitybots/api/bot/AbilityBotTest.java | 105 +++++++------- .../test/resources/messages_it_IT.properties | 2 +- 5 files changed, 171 insertions(+), 80 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index d4d2c2ea..95d39f5d 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -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 abilities() { + return abilities; + } + + /** + * @return the immutable list carrying the embedded replies + */ + public List replies() { + return replies; + } + /** * This method contains the stream of actions that are applied on any update. *

@@ -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: + *

+ * PUBLIC + *

+ * [command1] - [description1] + *

+ * [command2] - [description2] + *

+ * GROUP_ADMIN + *

+ * [command1] - [description1] + *

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

@@ -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 admins = admins(); - int id = creatorId(); + Set 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::builder, + (b, a) -> b.put(a.name(), a), + (b1, b2) -> b1.putAll(b2.build())) + .build(); Stream methodReplies = stream(this.getClass().getMethods()) .filter(method -> method.getReturnType().equals(Reply.class)) @@ -561,7 +639,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Stream abilityReplies = abilities.values().stream() .flatMap(ability -> ability.replies().stream()); - replies = Stream.concat(methodReplies, abilityReplies).collect(toList()); + replies = Stream.concat(methodReplies, abilityReplies).collect( + ImmutableList::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)); diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index a8495e84..57e2485b 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 808b4eda..fd1b75f9 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -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 diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index c2fa81b7..d7cdb9d7 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -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 actual = bot.blacklist(); - Set 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 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 trioOneArg = Trio.of(update, abilityWithOneInput, TEXT); Trio 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 docTrio = Trio.of(update, documentAbility, TEXT); Trio 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; - } } diff --git a/telegrambots-abilities/src/test/resources/messages_it_IT.properties b/telegrambots-abilities/src/test/resources/messages_it_IT.properties index c5656cc1..52f1377d 100644 --- a/telegrambots-abilities/src/test/resources/messages_it_IT.properties +++ b/telegrambots-abilities/src/test/resources/messages_it_IT.properties @@ -1 +1 @@ -ability.commands.notFound=Non sono presenti comandi pubblici. \ No newline at end of file +ability.commands.notFound=Non sono presenti comandi disponibile. \ No newline at end of file