Make abilities case-insensitive, fix msg markdown bug and add group-admin privacy

This commit is contained in:
Abbas Abou Daya 2018-02-04 03:23:41 -05:00
parent 066ad7f675
commit dc9f34196e
5 changed files with 122 additions and 24 deletions

View File

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Reply> 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("_", "\\_");
}
}

View File

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

View File

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

View File

@ -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<Update, Ability, String[]> publicTrio = Trio.of(update, publicAbility, TEXT);
Trio<Update, Ability, String[]> groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT);
Trio<Update, Ability, String[]> adminTrio = Trio.of(update, adminAbility, TEXT);
Trio<Update, Ability, String[]> 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<Update, Ability, String[]> 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<Update, Ability, String[]> 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<Update, Ability, String[]> 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);

View File

@ -31,7 +31,7 @@ public class MapDBContextTest {
}
@Test
public void canRecoverDB() throws IOException {
public void canRecoverDB() {
Map<Integer, EndUser> users = db.getMap(USERS);
Map<String, Integer> userIds = db.getMap(USER_ID);
users.put(CREATOR.id(), CREATOR);