diff --git a/TelegramBots.wiki/abilities/Simple-Example.md b/TelegramBots.wiki/abilities/Simple-Example.md index 3e2419e3..9b99679b 100644 --- a/TelegramBots.wiki/abilities/Simple-Example.md +++ b/TelegramBots.wiki/abilities/Simple-Example.md @@ -102,8 +102,9 @@ If you're in doubt that you're missing some code, the full code example can be i Go ahead and "/hello" to your bot. It should respond back with "Hello World!". Since you've implemented an AbilityBot, you get **factory abilities** as well. Try: -* /commands - Prints all commands supported by the bot - * This will essentially print "hello - says hello world!". Yes! This is the information we supplied to the ability. The bot prints the commands in the format accepted by BotFather. So, whenever you change, add or remove commands, you can simply /commands and forward that message to BotFather. +* /report - Prints all user-defined commands supported by the bot + * This will essentially print "hello - says hello world!". Yes! This is the information we supplied to the ability. The bot prints the commands in the format accepted by BotFather. So, whenever you change, add or remove commands, you can simply /report and forward that message to BotFather. +* /commands - Prints all commands exposed by the bot (factory and user-defined, with and without info) * /claim - Claims this bot * /backup - returns a backup of the bot database * /recover - recovers the database 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..a86841cb 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.*; @@ -64,10 +72,11 @@ import static org.telegram.abilitybots.api.util.AbilityUtils.*; *
  • Sets the user as the {@link Privacy#CREATOR} of the bot
  • *
  • Only the user with the ID returned by {@link AbilityBot#creatorId()} can genuinely claim the bot
  • * - *
  • /commands - reports all user-defined commands (abilities)
  • + *
  • /report - reports all user-defined commands (abilities)
  • * + *
  • /commands - returns a list of all possible bot commands based on the privacy of the requesting user
  • *
  • /backup - returns a backup of the bot database
  • *
  • /recover - recovers the database
  • *
  • /promote @username - promotes user to bot admin
  • @@ -102,6 +111,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 +182,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 +284,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 +314,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 +335,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 +588,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 +628,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 +640,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 +744,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 +757,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