Expose abilities and replies, add report command and reformat /commands, closes #436
This commit is contained in:
parent
b0b2504e01
commit
889fd46834
@ -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<String, Ability> abilities() {
|
||||
return abilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the immutable list carrying the embedded replies
|
||||
*/
|
||||
public List<Reply> replies() {
|
||||
return replies;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method contains the stream of actions that are applied on any update.
|
||||
* <p>
|
||||
@ -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:
|
||||
* <p>
|
||||
* PUBLIC
|
||||
* <p>
|
||||
* [command1] - [description1]
|
||||
* <p>
|
||||
* [command2] - [description2]
|
||||
* <p>
|
||||
* GROUP_ADMIN
|
||||
* <p>
|
||||
* [command1] - [description1]
|
||||
* <p>
|
||||
* ...
|
||||
*
|
||||
* @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<Privacy, String> 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.
|
||||
* <p>
|
||||
@ -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<Integer> admins = admins();
|
||||
int id = creatorId();
|
||||
Set<Integer> 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::<String, Ability>builder,
|
||||
(b, a) -> b.put(a.name(), a),
|
||||
(b1, b2) -> b1.putAll(b2.build()))
|
||||
.build();
|
||||
|
||||
Stream<Reply> methodReplies = stream(this.getClass().getMethods())
|
||||
.filter(method -> method.getReturnType().equals(Reply.class))
|
||||
@ -561,7 +639,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
|
||||
Stream<Reply> abilityReplies = abilities.values().stream()
|
||||
.flatMap(ability -> ability.replies().stream());
|
||||
|
||||
replies = Stream.concat(methodReplies, abilityReplies).collect(toList());
|
||||
replies = Stream.concat(methodReplies, abilityReplies).collect(
|
||||
ImmutableList::<Reply>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));
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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<Integer> actual = bot.blacklist();
|
||||
Set<Integer> 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<Update, Ability, String[]> 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<Update, Ability, String[]> trioOneArg = Trio.of(update, abilityWithOneInput, TEXT);
|
||||
Trio<Update, Ability, String[]> 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<Update, Ability, String[]> docTrio = Trio.of(update, documentAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> 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;
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
ability.commands.notFound=Non sono presenti comandi pubblici.
|
||||
ability.commands.notFound=Non sono presenti comandi disponibile.
|
Loading…
Reference in New Issue
Block a user