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.Pair;
import org.telegram.abilitybots.api.util.Trio; import org.telegram.abilitybots.api.util.Trio;
import org.telegram.telegrambots.api.methods.GetFile; 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.methods.send.SendDocument;
import org.telegram.telegrambots.api.objects.Message; import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.Update; import org.telegram.telegrambots.api.objects.Update;
@ -24,10 +25,7 @@ import java.io.FileReader;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -413,10 +411,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Set<Integer> blacklist = blacklist(); Set<Integer> blacklist = blacklist();
if (blacklist.contains(userId)) 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 { else {
blacklist.add(userId); 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)) .post(commitTo(db))
@ -441,9 +439,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Set<Integer> blacklist = blacklist(); Set<Integer> blacklist = blacklist();
if (!blacklist.remove(userId)) 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 { 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)) .post(commitTo(db))
@ -465,10 +463,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Set<Integer> admins = admins(); Set<Integer> admins = admins();
if (admins.contains(userId)) 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 { else {
admins.add(userId); 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)) }).post(commitTo(db))
.build(); .build();
@ -489,9 +487,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Set<Integer> admins = admins(); Set<Integer> admins = admins();
if (admins.remove(userId)) { 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 { } 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)) .post(commitTo(db))
@ -540,7 +538,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
abilities = stream(this.getClass().getMethods()) abilities = stream(this.getClass().getMethods())
.filter(method -> method.getReturnType().equals(Ability.class)) .filter(method -> method.getReturnType().equals(Ability.class))
.map(this::returnAbility) .map(this::returnAbility)
.collect(toMap(Ability::name, identity())); .collect(toMap(ability -> ability.name().toLowerCase(), identity()));
Stream<Reply> methodReplies = stream(this.getClass().getMethods()) Stream<Reply> methodReplies = stream(this.getClass().getMethods())
.filter(method -> method.getReturnType().equals(Reply.class)) .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); boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens);
if (!isOk) 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; return isOk;
} }
@ -629,7 +627,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
boolean isOk = abilityLocality == ALL || locality == abilityLocality; boolean isOk = abilityLocality == ALL || locality == abilityLocality;
if (!isOk) 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; return isOk;
} }
@ -639,15 +637,24 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Privacy privacy; Privacy privacy;
int id = user.id(); 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; boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
if (!isOk) 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; 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) { private boolean isCreator(int id) {
return id == creatorId(); return id == creatorId();
} }
@ -667,11 +674,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
if (!update.hasMessage() || !msg.hasText()) if (!update.hasMessage() || !msg.hasText())
return Trio.of(update, abilities.get(DEFAULT), new String[]{}); return Trio.of(update, abilities.get(DEFAULT), new String[]{});
// Priority goes to text before captions
String[] tokens = msg.getText().split(" "); String[] tokens = msg.getText().split(" ");
if (tokens[0].startsWith("/")) { if (tokens[0].startsWith("/")) {
String abilityToken = stripBotUsername(tokens[0].substring(1)); String abilityToken = stripBotUsername(tokens[0].substring(1)).toLowerCase();
Ability ability = abilities.get(abilityToken); Ability ability = abilities.get(abilityToken);
tokens = Arrays.copyOfRange(tokens, 1, tokens.length); tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
return Trio.of(update, ability, tokens); return Trio.of(update, ability, tokens);
@ -743,4 +749,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
private File downloadFileWithId(String fileId) throws TelegramApiException { private File downloadFileWithId(String fileId) throws TelegramApiException {
return sender.downloadFile(sender.execute(new GetFile().setFileId(fileId))); 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. * Anybody who is not a bot admin or its creator will be considered as a public user.
*/ */
PUBLIC, 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. * 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. * 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.collect.ImmutableMap;
import com.google.common.io.Files; import com.google.common.io.Files;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.After; import org.junit.After;
import org.junit.Before; 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.sender.SilentSender;
import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Pair;
import org.telegram.abilitybots.api.util.Trio; 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.api.objects.*;
import org.telegram.telegrambots.exceptions.TelegramApiException; import org.telegram.telegrambots.exceptions.TelegramApiException;
@ -21,11 +21,14 @@ import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newHashSet;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Collections.emptySet; 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.io.FileUtils.deleteQuietly;
import static org.apache.commons.lang3.ArrayUtils.addAll; import static org.apache.commons.lang3.ArrayUtils.addAll;
import static org.apache.commons.lang3.StringUtils.EMPTY; 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.ALL;
import static org.telegram.abilitybots.api.objects.Locality.GROUP; 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.MessageContext.newContext;
import static org.telegram.abilitybots.api.objects.Privacy.ADMIN; import static org.telegram.abilitybots.api.objects.Privacy.*;
import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
public class AbilityBotTest { public class AbilityBotTest {
private static final String[] EMPTY_ARRAY = {}; private static final String[] EMPTY_ARRAY = {};
@ -77,7 +79,7 @@ public class AbilityBotTest {
bot.onUpdateReceived(update); 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 @Test
@ -342,20 +344,61 @@ public class AbilityBotTest {
Message message = mock(Message.class); Message message = mock(Message.class);
org.telegram.telegrambots.api.objects.User user = mock(User.class); org.telegram.telegrambots.api.objects.User user = mock(User.class);
Ability publicAbility = getDefaultBuilder().privacy(PUBLIC).build(); Ability publicAbility = getDefaultBuilder().privacy(PUBLIC).build();
Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build();
Ability adminAbility = getDefaultBuilder().privacy(ADMIN).build(); Ability adminAbility = getDefaultBuilder().privacy(ADMIN).build();
Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build(); Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build();
Trio<Update, Ability, String[]> publicTrio = Trio.of(update, publicAbility, TEXT); 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[]> adminTrio = Trio.of(update, adminAbility, TEXT);
Trio<Update, Ability, String[]> creatorTrio = Trio.of(update, creatorAbility, TEXT); Trio<Update, Ability, String[]> creatorTrio = Trio.of(update, creatorAbility, TEXT);
mockUser(update, message, user); mockUser(update, message, user);
assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(publicTrio)); 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(adminTrio));
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio)); 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 @Test
public void canBlockAdminsFromCreatorAbilities() { public void canBlockAdminsFromCreatorAbilities() {
Update update = mock(Update.class); Update update = mock(Update.class);
@ -447,6 +490,25 @@ public class AbilityBotTest {
assertEquals("Wrong ability was fetched", expected, actual); 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 @Test
public void canFetchDefaultAbility() { public void canFetchDefaultAbility() {
Update update = mock(Update.class); Update update = mock(Update.class);

View File

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