From dc9f34196e3a97145a7d5ab6a457d27b4cc5c39a Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Sun, 4 Feb 2018 03:23:41 -0500 Subject: [PATCH] Make abilities case-insensitive, fix msg markdown bug and add group-admin privacy --- .../abilitybots/api/bot/AbilityBot.java | 48 ++++++++----- .../abilitybots/api/objects/Privacy.java | 4 ++ .../abilitybots/api/util/AbilityUtils.java | 22 ++++++ .../abilitybots/api/bot/AbilityBotTest.java | 70 +++++++++++++++++-- .../abilitybots/api/db/MapDBContextTest.java | 2 +- 5 files changed, 122 insertions(+), 24 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 c512e7c4..a9c4e0e3 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 @@ -10,6 +10,7 @@ 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.api.methods.GetFile; +import org.telegram.telegrambots.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.api.methods.send.SendDocument; import org.telegram.telegrambots.api.objects.Message; import org.telegram.telegrambots.api.objects.Update; @@ -24,10 +25,7 @@ import java.io.FileReader; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Stream; @@ -413,10 +411,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(format("%s is already *banned*.", bannedUser), ctx.chatId()); + silent.sendMd(format("%s is already *banned*.", escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(format("%s is now *banned*.", bannedUser), ctx.chatId()); + silent.sendMd(format("%s is now *banned*.", escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -441,9 +439,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(format("@%s is *not* on the *blacklist*.", username), ctx.chatId()); + silent.sendMd(format("@%s is *not* on the *blacklist*.", escape(username)), ctx.chatId()); else { - silent.sendMd(format("@%s, your ban has been *lifted*.", username), ctx.chatId()); + silent.sendMd(format("@%s, your ban has been *lifted*.", escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -465,10 +463,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(format("@%s is already an *admin*.", username), ctx.chatId()); + silent.sendMd(format("@%s is already an *admin*.", escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(format("@%s has been *promoted*.", username), ctx.chatId()); + silent.sendMd(format("@%s has been *promoted*.", escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -489,9 +487,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(format("@%s has been *demoted*.", username), ctx.chatId()); + silent.sendMd(format("@%s has been *demoted*.", escape(username)), ctx.chatId()); } else { - silent.sendMd(format("@%s is *not* an *admin*.", username), ctx.chatId()); + silent.sendMd(format("@%s is *not* an *admin*.", escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -540,7 +538,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { abilities = stream(this.getClass().getMethods()) .filter(method -> method.getReturnType().equals(Ability.class)) .map(this::returnAbility) - .collect(toMap(Ability::name, identity())); + .collect(toMap(ability -> ability.name().toLowerCase(), identity())); Stream methodReplies = stream(this.getClass().getMethods()) .filter(method -> method.getReturnType().equals(Reply.class)) @@ -617,7 +615,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens); if (!isOk) - silent.send(String.format("Sorry, this feature requires %d additional %s.", abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a())); + silent.send(format("Sorry, this feature requires %d additional %s.", abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a())); return isOk; } @@ -629,7 +627,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { boolean isOk = abilityLocality == ALL || locality == abilityLocality; if (!isOk) - silent.send(String.format("Sorry, %s-only feature.", abilityLocality.toString().toLowerCase()), getChatId(trio.a())); + silent.send(format("Sorry, %s-only feature.", abilityLocality.toString().toLowerCase()), getChatId(trio.a())); return isOk; } @@ -639,15 +637,24 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Privacy privacy; int id = user.id(); - privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : PUBLIC; + privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : isGroupAdmin(update, id)? GROUP_ADMIN : PUBLIC; boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0; if (!isOk) - silent.send(String.format("Sorry, %s-only feature.", trio.b().privacy().toString().toLowerCase()), getChatId(trio.a())); + silent.send("Sorry, you don't have the required access level to do that.", getChatId(trio.a())); + return isOk; } + private boolean isGroupAdmin(Update update, int id) { + GetChatAdministrators admins = new GetChatAdministrators().setChatId(getChatId(update)); + + return isGroupUpdate(update) && silent.execute(admins) + .orElse(new ArrayList<>()).stream() + .anyMatch(member -> member.getUser().getId() == id); + } + private boolean isCreator(int id) { return id == creatorId(); } @@ -667,11 +674,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!update.hasMessage() || !msg.hasText()) return Trio.of(update, abilities.get(DEFAULT), new String[]{}); - // Priority goes to text before captions String[] tokens = msg.getText().split(" "); if (tokens[0].startsWith("/")) { - String abilityToken = stripBotUsername(tokens[0].substring(1)); + String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase(); Ability ability = abilities.get(abilityToken); tokens = Arrays.copyOfRange(tokens, 1, tokens.length); return Trio.of(update, ability, tokens); @@ -743,4 +749,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { private File downloadFileWithId(String fileId) throws TelegramApiException { return sender.downloadFile(sender.execute(new GetFile().setFileId(fileId))); } + + private String escape(String username) { + return username.replace("_", "\\_"); + } } \ No newline at end of file diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Privacy.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Privacy.java index 90d13cef..51d40937 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Privacy.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Privacy.java @@ -10,6 +10,10 @@ public enum Privacy { * Anybody who is not a bot admin or its creator will be considered as a public user. */ PUBLIC, + /** + * Only group admins would get to initiate this command. + */ + GROUP_ADMIN, /** * A global admin of the bot, regardless of the group the bot is in. */ diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index d0586039..8852170f 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -64,6 +64,28 @@ public final class AbilityUtils { } } + /** + * A "best-effort" boolean stating whether the update is a group message or not. + * + * @param update a Telegram {@link Update} + * @return whether the update is linked to a group + */ + public static boolean isGroupUpdate(Update update) { + if (MESSAGE.test(update)) { + return update.getMessage().isGroupMessage(); + } else if (CALLBACK_QUERY.test(update)) { + return update.getCallbackQuery().getMessage().isGroupMessage(); + } else if (CHANNEL_POST.test(update)) { + return update.getChannelPost().isGroupMessage(); + } else if (EDITED_CHANNEL_POST.test(update)) { + return update.getEditedChannelPost().isGroupMessage(); + } else if (EDITED_MESSAGE.test(update)) { + return update.getEditedMessage().isGroupMessage(); + } else { + return false; + } + } + /** * Fetches the direct chat ID of the specified update. * 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 634d222d..ff335720 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 @@ -2,7 +2,6 @@ package org.telegram.abilitybots.api.bot; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; -import org.apache.commons.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; @@ -13,6 +12,7 @@ import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender; import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Trio; +import org.telegram.telegrambots.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.api.objects.*; import org.telegram.telegrambots.exceptions.TelegramApiException; @@ -21,11 +21,14 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Map; +import java.util.Optional; import java.util.Set; +import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static java.lang.String.format; import static java.util.Collections.emptySet; +import static java.util.Optional.empty; import static org.apache.commons.io.FileUtils.deleteQuietly; import static org.apache.commons.lang3.ArrayUtils.addAll; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -43,8 +46,7 @@ import static org.telegram.abilitybots.api.objects.Flag.MESSAGE; 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.MessageContext.newContext; -import static org.telegram.abilitybots.api.objects.Privacy.ADMIN; -import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; +import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { private static final String[] EMPTY_ARRAY = {}; @@ -77,7 +79,7 @@ public class AbilityBotTest { bot.onUpdateReceived(update); - verify(silent, times(1)).send(format("Sorry, %s-only feature.", "admin"), MUSER.id()); + verify(silent, times(1)).send("Sorry, you don't have the required access level to do that.", MUSER.id()); } @Test @@ -342,20 +344,61 @@ public class AbilityBotTest { Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); Ability publicAbility = getDefaultBuilder().privacy(PUBLIC).build(); + Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); Ability adminAbility = getDefaultBuilder().privacy(ADMIN).build(); Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build(); Trio publicTrio = Trio.of(update, publicAbility, TEXT); + Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); Trio adminTrio = Trio.of(update, adminAbility, TEXT); Trio creatorTrio = Trio.of(update, creatorAbility, TEXT); 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)); } + @Test + public void canValidateGroupAdminPrivacy() throws TelegramApiException { + Update update = mock(Update.class); + Message message = mock(Message.class); + org.telegram.telegrambots.api.objects.User user = mock(User.class); + Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); + + Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); + + mockUser(update, message, user); + when(message.isGroupMessage()).thenReturn(true); + + ChatMember member = mock(ChatMember.class); + when(member.getUser()).thenReturn(user); + when(member.getUser()).thenReturn(user); + + when(silent.execute(any(GetChatAdministrators.class))).thenReturn(Optional.of(newArrayList(member))); + + assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(groupAdminTrio)); + } + + @Test + public void canRestrictNormalUsersFromGroupAdminAbilities() throws TelegramApiException { + Update update = mock(Update.class); + Message message = mock(Message.class); + org.telegram.telegrambots.api.objects.User user = mock(User.class); + Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); + + Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); + + mockUser(update, message, user); + when(message.isGroupMessage()).thenReturn(true); + + when(silent.execute(any(GetChatAdministrators.class))).thenReturn(empty()); + + assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(groupAdminTrio)); + } + @Test public void canBlockAdminsFromCreatorAbilities() { Update update = mock(Update.class); @@ -447,6 +490,25 @@ public class AbilityBotTest { assertEquals("Wrong ability was fetched", expected, actual); } + @Test + public void canFetchAbilityCaseInsensitive() { + Update update = mock(Update.class); + Message message = mock(Message.class); + + String text = "/tESt"; + when(update.hasMessage()).thenReturn(true); + when(update.getMessage()).thenReturn(message); + when(update.getMessage().hasText()).thenReturn(true); + when(message.getText()).thenReturn(text); + + Trio trio = bot.getAbility(update); + + Ability expected = bot.testAbility(); + Ability actual = trio.b(); + + assertEquals("Wrong ability was fetched", expected, actual); + } + @Test public void canFetchDefaultAbility() { Update update = mock(Update.class); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java index 2d9a6f16..7e53584b 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java @@ -31,7 +31,7 @@ public class MapDBContextTest { } @Test - public void canRecoverDB() throws IOException { + public void canRecoverDB() { Map users = db.getMap(USERS); Map userIds = db.getMap(USER_ID); users.put(CREATOR.id(), CREATOR);