Merge pull request #449 from davioooh/abilities-i18n

Abilities i18n
This commit is contained in:
Ruben Bermudez 2018-05-23 20:03:59 +02:00 committed by GitHub
commit b0b2504e01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 414 additions and 311 deletions

View File

@ -14,6 +14,7 @@ import org.telegram.telegrambots.api.methods.groupadministration.GetChatAdminist
import org.telegram.telegrambots.api.methods.send.SendDocument;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.Update;
import org.telegram.telegrambots.api.objects.User;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.exceptions.TelegramApiException;
@ -44,11 +45,11 @@ import static java.util.stream.Collectors.toMap;
import static jersey.repackaged.com.google.common.base.Throwables.propagate;
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.EndUser.fromUser;
import static org.telegram.abilitybots.api.objects.Flag.*;
import static org.telegram.abilitybots.api.objects.Locality.*;
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
import static org.telegram.abilitybots.api.objects.Privacy.*;
import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
import static org.telegram.abilitybots.api.util.AbilityUtils.*;
/**
@ -102,10 +103,6 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
protected static final String RECOVER = "recover";
protected static final String COMMANDS = "commands";
// Messages
protected static final String RECOVERY_MESSAGE = "I am ready to receive the backup file. Please reply to this message with the backup file attached.";
protected static final String RECOVER_SUCCESS = "I have successfully recovered.";
// DB and sender
protected final DBContext db;
protected MessageSender sender;
@ -148,9 +145,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
}
/**
* @return the map of ID -> EndUser
* @return the map of ID -> User
*/
protected Map<Integer, EndUser> users() {
protected Map<Integer, User> users() {
return db.getMap(USERS);
}
@ -235,7 +232,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
* @param username the username of the required user
* @return the user
*/
protected EndUser getUser(String username) {
protected User getUser(String username) {
Integer id = userIds().get(username.toLowerCase());
if (id == null) {
throw new IllegalStateException(format("Could not find ID corresponding to username [%s]", username));
@ -250,26 +247,27 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
* @param id the id of the required user
* @return the user
*/
protected EndUser getUser(int id) {
EndUser endUser = users().get(id);
if (endUser == null) {
protected User getUser(int id) {
User user = users().get(id);
if (user == null) {
throw new IllegalStateException(format("Could not find user corresponding to id [%d]", id));
}
return endUser;
return user;
}
/**
* 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
* @return the id of the user
*/
protected int getUserIdSendError(String username, long chatId) {
protected int getUserIdSendError(String username, MessageContext ctx) {
try {
return getUser(username).id();
return getUser(username).getId();
} catch (IllegalStateException ex) {
silent.send(format("Sorry, I could not find the user [%s].", username), chatId);
silent.send(getLocalizedMessage(USER_NOT_FOUND, ctx.user().getLanguageCode(), username), ctx.chatId());
throw propagate(ex);
}
}
@ -306,7 +304,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
})
.sorted()
.reduce((a, b) -> format("%s%n%s", a, b))
.orElse("No public commands found.");
.orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode()));
silent.send(commands, ctx.chatId());
})
@ -361,23 +359,27 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.locality(USER)
.privacy(CREATOR)
.input(0)
.action(ctx -> silent.forceReply(RECOVERY_MESSAGE, ctx.chatId()))
.action(ctx -> silent.forceReply(
getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().getLanguageCode()), ctx.chatId()))
.reply(update -> {
Long chatId = update.getMessage().getChatId();
String fileId = update.getMessage().getDocument().getFileId();
String replyToMsg = update.getMessage().getReplyToMessage().getText();
String recoverMessage = getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(update).getLanguageCode());
if (!replyToMsg.equals(recoverMessage))
return;
String fileId = update.getMessage().getDocument().getFileId();
try (FileReader reader = new FileReader(downloadFileWithId(fileId))) {
String backupData = IOUtils.toString(reader);
if (db.recover(backupData)) {
silent.send(RECOVER_SUCCESS, chatId);
send(ABILITY_RECOVER_SUCCESS, update);
} else {
silent.send("Oops, something went wrong during recovery.", chatId);
send(ABILITY_RECOVER_FAIL, update);
}
} catch (Exception e) {
BotLogger.error("Could not recover DB from backup", TAG, e);
silent.send("I have failed to recover.", chatId);
send(ABILITY_RECOVER_ERROR, update);
}
}, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE))
}, MESSAGE, DOCUMENT, REPLY)
.build();
}
@ -398,23 +400,23 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.input(1)
.action(ctx -> {
String username = stripTag(ctx.firstArg());
int userId = getUserIdSendError(username, ctx.chatId());
int userId = getUserIdSendError(username, ctx);
String bannedUser;
// Protection from abuse
if (userId == creatorId()) {
userId = ctx.user().id();
bannedUser = isNullOrEmpty(ctx.user().username()) ? addTag(ctx.user().username()) : ctx.user().shortName();
userId = ctx.user().getId();
bannedUser = isNullOrEmpty(ctx.user().getUserName()) ? addTag(ctx.user().getUserName()) : shortName(ctx.user());
} else {
bannedUser = addTag(username);
}
Set<Integer> blacklist = blacklist();
if (blacklist.contains(userId))
silent.sendMd(format("%s is already *banned*.", escape(bannedUser)), ctx.chatId());
sendMd(ABILITY_BAN_FAIL, ctx, escape(bannedUser));
else {
blacklist.add(userId);
silent.sendMd(format("%s is now *banned*.", escape(bannedUser)), ctx.chatId());
sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser));
}
})
.post(commitTo(db))
@ -434,14 +436,14 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.input(1)
.action(ctx -> {
String username = stripTag(ctx.firstArg());
Integer userId = getUserIdSendError(username, ctx.chatId());
Integer userId = getUserIdSendError(username, ctx);
Set<Integer> blacklist = blacklist();
if (!blacklist.remove(userId))
silent.sendMd(format("@%s is *not* on the *blacklist*.", escape(username)), ctx.chatId());
silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
else {
silent.sendMd(format("@%s, your ban has been *lifted*.", escape(username)), ctx.chatId());
silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
}
})
.post(commitTo(db))
@ -459,14 +461,14 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.input(1)
.action(ctx -> {
String username = stripTag(ctx.firstArg());
Integer userId = getUserIdSendError(username, ctx.chatId());
Integer userId = getUserIdSendError(username, ctx);
Set<Integer> admins = admins();
if (admins.contains(userId))
silent.sendMd(format("@%s is already an *admin*.", escape(username)), ctx.chatId());
sendMd(ABILITY_PROMOTE_FAIL, ctx, escape(username));
else {
admins.add(userId);
silent.sendMd(format("@%s has been *promoted*.", escape(username)), ctx.chatId());
sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username));
}
}).post(commitTo(db))
.build();
@ -483,13 +485,13 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.input(1)
.action(ctx -> {
String username = stripTag(ctx.firstArg());
Integer userId = getUserIdSendError(username, ctx.chatId());
Integer userId = getUserIdSendError(username, ctx);
Set<Integer> admins = admins();
if (admins.remove(userId)) {
silent.sendMd(format("@%s has been *demoted*.", escape(username)), ctx.chatId());
sendMd(ABILITY_DEMOTE_SUCCESS, ctx, escape(username));
} else {
silent.sendMd(format("@%s is *not* an *admin*.", escape(username)), ctx.chatId());
sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username));
}
})
.post(commitTo(db))
@ -508,26 +510,38 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
.privacy(PUBLIC)
.input(0)
.action(ctx -> {
if (ctx.user().id() == creatorId()) {
if (ctx.user().getId() == creatorId()) {
Set<Integer> admins = admins();
int id = creatorId();
long chatId = ctx.chatId();
if (admins.contains(id))
silent.send("You're already my master.", chatId);
send(ABILITY_CLAIM_FAIL, ctx);
else {
admins.add(id);
silent.send("You're now my master.", chatId);
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().username()));
abilities.get(BAN).action().accept(newContext(ctx.update(), ctx.user(), ctx.chatId(), ctx.user().getUserName()));
}
})
.post(commitTo(db))
.build();
}
private Optional<Message> send(String message, MessageContext ctx, String... args) {
return silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> sendMd(String message, MessageContext ctx, String... args) {
return silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId());
}
private Optional<Message> send(String message, Update upd) {
Long chatId = upd.getMessage().getChatId();
return silent.send(getLocalizedMessage(message, AbilityUtils.getUser(upd).getLanguageCode()), chatId);
}
/**
* Registers the declared abilities using method reflection. Also, replies are accumulated using the built abilities and standalone methods that return a Reply.
* <p>
@ -597,7 +611,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
Update update = trio.a();
EndUser user = fromUser(AbilityUtils.getUser(update));
User user = AbilityUtils.getUser(update);
return Pair.of(newContext(update, user, getChatId(update), trio.c()), trio.b());
}
@ -615,7 +629,12 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens);
if (!isOk)
silent.send(format("Sorry, this feature requires %d additional %s.", abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a()));
silent.send(
getLocalizedMessage(
CHECK_INPUT_FAIL,
AbilityUtils.getUser(trio.a()).getLanguageCode(),
abilityTokens, abilityTokens == 1 ? "input" : "inputs"),
getChatId(trio.a()));
return isOk;
}
@ -627,23 +646,31 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
boolean isOk = abilityLocality == ALL || locality == abilityLocality;
if (!isOk)
silent.send(format("Sorry, %s-only feature.", abilityLocality.toString().toLowerCase()), getChatId(trio.a()));
silent.send(
getLocalizedMessage(
CHECK_LOCALITY_FAIL,
AbilityUtils.getUser(trio.a()).getLanguageCode(),
abilityLocality.toString().toLowerCase()),
getChatId(trio.a()));
return isOk;
}
boolean checkPrivacy(Trio<Update, Ability, String[]> trio) {
Update update = trio.a();
EndUser user = fromUser(AbilityUtils.getUser(update));
User user = AbilityUtils.getUser(update);
Privacy privacy;
int id = user.id();
int id = user.getId();
privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id)? GROUP_ADMIN : PUBLIC;
privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : (isGroupUpdate(update) || isSuperGroupUpdate(update)) && isGroupAdmin(update, id) ? GROUP_ADMIN : PUBLIC;
boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
if (!isOk)
silent.send("Sorry, you don't have the required access level to do that.", getChatId(trio.a()));
silent.send(
getLocalizedMessage(
CHECK_PRIVACY_FAIL,
AbilityUtils.getUser(trio.a()).getLanguageCode()),
getChatId(trio.a()));
return isOk;
}
@ -694,9 +721,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
}
Update addUser(Update update) {
EndUser endUser = fromUser(AbilityUtils.getUser(update));
User endUser = AbilityUtils.getUser(update);
users().compute(endUser.id(), (id, user) -> {
users().compute(endUser.getId(), (id, user) -> {
if (user == null) {
updateUserId(user, endUser);
return endUser;
@ -714,15 +741,15 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
return update;
}
private void updateUserId(EndUser oldUser, EndUser newUser) {
if (oldUser != null && oldUser.username() != null) {
private void updateUserId(User oldUser, User newUser) {
if (oldUser != null && oldUser.getUserName() != null) {
// Remove old username -> ID
userIds().remove(oldUser.username());
userIds().remove(oldUser.getUserName());
}
if (newUser.username() != null) {
if (newUser.getUserName() != null) {
// Add new mapping with the new username
userIds().put(newUser.username().toLowerCase(), newUser.id());
userIds().put(newUser.getUserName().toLowerCase(), newUser.getId());
}
}
@ -750,6 +777,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot {
return sender.downloadFile(sender.execute(new GetFile().setFileId(fileId)));
}
private String escape(String username) {
return username.replace("_", "\\_");
}

View File

@ -1,138 +0,0 @@
package org.telegram.abilitybots.api.objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;
import org.telegram.telegrambots.api.objects.User;
import java.io.Serializable;
import java.util.Objects;
import java.util.StringJoiner;
import static org.apache.commons.lang3.StringUtils.isEmpty;
/**
* This class serves the purpose of separating the basic Telegram {@link User} and the augmented {@link EndUser}.
* <p>
* It adds proper hashCode, equals, toString as well as useful utility methods such as {@link EndUser#shortName} and {@link EndUser#fullName}.
*
* @author Abbas Abou Daya
*/
public final class EndUser implements Serializable {
@JsonProperty("id")
private final Integer id;
@JsonProperty("firstName")
private final String firstName;
@JsonProperty("lastName")
private final String lastName;
@JsonProperty("username")
private final String username;
private EndUser(Integer id, String firstName, String lastName, String username) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.username = username;
}
@JsonCreator
public static EndUser endUser(@JsonProperty("id") Integer id,
@JsonProperty("firstName") String firstName,
@JsonProperty("lastName") String lastName,
@JsonProperty("username") String username) {
return new EndUser(id, firstName, lastName, username);
}
/**
* Constructs an {@link EndUser} from a {@link User}.
*
* @param user the Telegram user
* @return an augmented end-user
*/
public static EndUser fromUser(User user) {
return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName());
}
public int id() {
return id;
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
public String username() {
return username;
}
/**
* The full name is identified as the concatenation of the first and last name, separated by a space.
* This method can return an empty name if both first and last name are empty.
*
* @return the full name of the user
*/
public String fullName() {
StringJoiner name = new StringJoiner(" ");
if (!isEmpty(firstName))
name.add(firstName);
if (!isEmpty(lastName))
name.add(lastName);
return name.toString();
}
/**
* The short name is one of the following:
* <ol>
* <li>First name</li>
* <li>Last name</li>
* <li>Username</li>
* </ol>
* The method will try to return the first valid name in the specified order.
*
* @return the short name of the user
*/
public String shortName() {
if (!isEmpty(firstName))
return firstName;
if (!isEmpty(lastName))
return lastName;
return username;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
EndUser endUser = (EndUser) o;
return Objects.equals(id, endUser.id) &&
Objects.equals(firstName, endUser.firstName) &&
Objects.equals(lastName, endUser.lastName) &&
Objects.equals(username, endUser.username);
}
@Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, username);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("firstName", firstName)
.add("lastName", lastName)
.add("username", username)
.toString();
}
}

View File

@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.objects;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import org.telegram.telegrambots.api.objects.Update;
import org.telegram.telegrambots.api.objects.User;
import java.util.Arrays;
@ -14,26 +15,26 @@ import java.util.Arrays;
* @author Abbas Abou Daya
*/
public class MessageContext {
private final EndUser user;
private final User user;
private final Long chatId;
private final String[] arguments;
private final Update update;
private MessageContext(Update update, EndUser user, Long chatId, String[] arguments) {
private MessageContext(Update update, User user, Long chatId, String[] arguments) {
this.user = user;
this.chatId = chatId;
this.update = update;
this.arguments = arguments;
}
public static MessageContext newContext(Update update, EndUser user, Long chatId, String... arguments) {
public static MessageContext newContext(Update update, User user, Long chatId, String... arguments) {
return new MessageContext(update, user, chatId, arguments);
}
/**
* @return the originating Telegram user of this update
*/
public EndUser user() {
public User user() {
return user;
}

View File

@ -14,7 +14,7 @@ import static java.util.Arrays.asList;
/**
* A reply consists of update conditionals and an action to be applied on the update.
* <p>
* If an update satisfies the {@link Reply#conditions}set by the reply, then it's safe to {@link Reply#actOn(Update)}.
* If an update satisfies the {@link Reply#conditions} set by the reply, then it's safe to {@link Reply#actOn(Update)}.
*
* @author Abbas Abou Daya
*/

View File

@ -0,0 +1,31 @@
package org.telegram.abilitybots.api.util;
public final class AbilityMessageCodes {
public static String USER_NOT_FOUND = "userNotFound";
public static String CHECK_INPUT_FAIL = "checkInput.fail";
public static String CHECK_LOCALITY_FAIL = "checkLocality.fail";
public static String CHECK_PRIVACY_FAIL = "checkPrivacy.fail";
public static String ABILITY_COMMANDS_NOT_FOUND = "ability.commands.notFound";
public static String ABILITY_RECOVER_SUCCESS = "ability.recover.success";
public static String ABILITY_RECOVER_FAIL = "ability.recover.fail";
public static String ABILITY_RECOVER_MESSAGE = "ability.recover.message";
public static String ABILITY_RECOVER_ERROR = "ability.recover.error";
public static String ABILITY_BAN_SUCCESS = "ability.ban.success";
public static String ABILITY_BAN_FAIL = "ability.ban.fail";
public static String ABILITY_UNBAN_SUCCESS = "ability.unban.success";
public static String ABILITY_UNBAN_FAIL = "ability.unban.fail";
public static String ABILITY_PROMOTE_SUCCESS = "ability.promote.success";
public static String ABILITY_PROMOTE_FAIL = "ability.promote.fail";
public static String ABILITY_DEMOTE_SUCCESS = "ability.demote.success";
public static String ABILITY_DEMOTE_FAIL = "ability.demote.fail";
public static String ABILITY_CLAIM_SUCCESS = "ability.claim.success";
public static String ABILITY_CLAIM_FAIL = "ability.claim.fail";
}

View File

@ -1,13 +1,23 @@
package org.telegram.abilitybots.api.util;
import com.google.common.base.Strings;
import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.MessageContext;
import org.telegram.telegrambots.api.objects.Update;
import org.telegram.telegrambots.api.objects.User;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static java.util.ResourceBundle.Control.FORMAT_PROPERTIES;
import static java.util.ResourceBundle.Control.getNoFallbackControl;
import static java.util.ResourceBundle.getBundle;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.telegram.abilitybots.api.objects.Flag.*;
/**
@ -172,4 +182,66 @@ public final class AbilityUtils {
public static Predicate<Update> isReplyTo(String msg) {
return update -> update.getMessage().getReplyToMessage().getText().equals(msg);
}
}
public static String getLocalizedMessage(String messageCode, Locale locale, Object...arguments) {
ResourceBundle bundle;
if (locale == null) {
bundle = getBundle("messages", Locale.ROOT);
} else {
try {
bundle = getBundle(
"messages",
locale,
getNoFallbackControl(FORMAT_PROPERTIES));
} catch (MissingResourceException e) {
bundle = getBundle("messages", Locale.ROOT);
}
}
String message = bundle.getString(messageCode);
return MessageFormat.format(message, arguments);
}
public static String getLocalizedMessage(String messageCode, String languageCode, Object...arguments){
Locale locale = Strings.isNullOrEmpty(languageCode) ? null : Locale.forLanguageTag(languageCode);
return getLocalizedMessage(messageCode, locale, arguments);
}
/**
* The short name is one of the following:
* <ol>
* <li>First name</li>
* <li>Last name</li>
* <li>Username</li>
* </ol>
* The method will try to return the first valid name in the specified order.
*
* @return the short name of the user
*/
public static String shortName(User user) {
if (!isEmpty(user.getFirstName()))
return user.getFirstName();
if (!isEmpty(user.getLastName()))
return user.getLastName();
return user.getUserName();
}
/**
* The full name is identified as the concatenation of the first and last name, separated by a space.
* This method can return an empty name if both first and last name are empty.
*
* @return the full name of the user
* @param user
*/
public static String fullName(User user) {
StringJoiner name = new StringJoiner(" ");
if (!isEmpty(user.getFirstName()))
name.add(user.getFirstName());
if (!isEmpty(user.getLastName()))
name.add(user.getLastName());
return name.toString();
}
}

View File

@ -0,0 +1,27 @@
ability.commands.notFound=No public commands found.
ability.recover.success=I have successfully recovered.
ability.recover.fail=Oops, something went wrong during recovery.
ability.recover.message=I am ready to receive the backup file. Please reply to this message with the backup file attached.
ability.recover.error=I have failed to recover.
ability.ban.success={0} is now *banned*.
ability.ban.fail={0} is already *banned*.
ability.unban.success=@{0}, your ban has been *lifted*.
ability.unban.fail=@{0} is *not* on the *blacklist*.
ability.promote.success=@{0} has been *promoted*.
ability.promote.fail=@{0} is already an *admin*.
ability.demote.success=@{0} has been *demoted*.
ability.demote.fail=@{0} is *not* an *admin*.
ability.claim.success=You''re now my master.
ability.claim.fail=You''re already my master.
checkInput.fail=Sorry, this feature requires {0,number,integer} additional {1}.
checkLocality.fail=Sorry, {0}-only feature.
checkPrivacy.fail=Sorry, you don''t have the required access level to do that.
userNotFound=Sorry, I could not find the user [{0}].

View File

@ -0,0 +1,81 @@
package org.telegram.abilitybots.api.bot;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.MessageContext;
import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.api.objects.User;
import java.io.IOException;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.telegram.abilitybots.api.bot.AbilityBotTest.mockContext;
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
public class AbilityBotI18nTest {
private static final User NO_LANGUAGE_USER = new User(1, "first", false, "last", "username", null);
private static final User ITALIAN_USER = new User(2, "first", false, "last", "username", "it-IT");
private DBContext db;
private NoPublicCommandsBot bot;
private MessageSender sender;
private SilentSender silent;
@Before
public void setUp() {
db = offlineInstance("db");
bot = new NoPublicCommandsBot(EMPTY, EMPTY, db);
sender = mock(MessageSender.class);
silent = mock(SilentSender.class);
bot.sender = sender;
bot.silent = silent;
}
@Test
public void missingPublicCommandsLocalizedCorrectly1() {
MessageContext context = mockContext(NO_LANGUAGE_USER);
bot.reportCommands().action().accept(context);
verify(silent, times(1))
.send("No public commands found.", NO_LANGUAGE_USER.getId());
}
@Test
public void missingPublicCommandsLocalizedCorrectly2() {
MessageContext context = mockContext(ITALIAN_USER);
bot.reportCommands().action().accept(context);
verify(silent, times(1))
.send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId());
}
@After
public void tearDown() throws IOException {
db.clear();
db.close();
}
public static class NoPublicCommandsBot extends AbilityBot {
protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) {
super(botToken, botUsername, db);
}
@Override
public int creatorId() {
return 1;
}
}
}

View File

@ -36,11 +36,8 @@ import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVERY_MESSAGE;
import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVER_SUCCESS;
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
import static org.telegram.abilitybots.api.objects.EndUser.endUser;
import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT;
import static org.telegram.abilitybots.api.objects.Flag.MESSAGE;
import static org.telegram.abilitybots.api.objects.Locality.ALL;
@ -49,12 +46,16 @@ import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
import static org.telegram.abilitybots.api.objects.Privacy.*;
public class AbilityBotTest {
// Messages
private static final String RECOVERY_MESSAGE = "I am ready to receive the backup file. Please reply to this message with the backup file attached.";
private static final String RECOVER_SUCCESS = "I have successfully recovered.";
private static final String[] EMPTY_ARRAY = {};
private static final long GROUP_ID = 10L;
private static final String TEST = "test";
private static final String[] TEXT = {TEST};
public static final EndUser MUSER = endUser(1, "first", "last", "username");
public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername");
public static final User USER = new User(1, "first", false, "last", "username", null);
public static final User CREATOR = new User(1337, "creatorFirst", false, "creatorLast", "creatorUsername", null);
private DefaultBot bot;
private DBContext db;
@ -75,39 +76,39 @@ public class AbilityBotTest {
@Test
public void sendsPrivacyViolation() {
Update update = mockFullUpdate(MUSER, "/admin");
Update update = mockFullUpdate(USER, "/admin");
bot.onUpdateReceived(update);
verify(silent, times(1)).send("Sorry, you don't have the required access level to do that.", MUSER.id());
verify(silent, times(1)).send("Sorry, you don't have the required access level to do that.", USER.getId());
}
@Test
public void sendsLocalityViolation() {
Update update = mockFullUpdate(MUSER, "/group");
Update update = mockFullUpdate(USER, "/group");
bot.onUpdateReceived(update);
verify(silent, times(1)).send(format("Sorry, %s-only feature.", "group"), MUSER.id());
verify(silent, times(1)).send(format("Sorry, %s-only feature.", "group"), USER.getId());
}
@Test
public void sendsInputArgsViolation() {
Update update = mockFullUpdate(MUSER, "/count 1 2 3");
Update update = mockFullUpdate(USER, "/count 1 2 3");
bot.onUpdateReceived(update);
verify(silent, times(1)).send(format("Sorry, this feature requires %d additional inputs.", 4), MUSER.id());
verify(silent, times(1)).send(format("Sorry, this feature requires %d additional inputs.", 4), USER.getId());
}
@Test
public void canProcessRepliesIfSatisfyRequirements() {
Update update = mockFullUpdate(MUSER, "must reply");
Update update = mockFullUpdate(USER, "must reply");
// False means the update was not pushed down the stream since it has been consumed by the reply
assertFalse(bot.filterReply(update));
verify(silent, times(1)).send("reply", MUSER.id());
verify(silent, times(1)).send("reply", USER.getId());
}
@Test
@ -144,8 +145,8 @@ public class AbilityBotTest {
@Test
public void canDemote() {
addUsers(MUSER);
bot.admins().add(MUSER.id());
addUsers(USER);
bot.admins().add(USER.getId());
MessageContext context = defaultContext();
@ -158,33 +159,33 @@ public class AbilityBotTest {
@Test
public void canPromote() {
addUsers(MUSER);
addUsers(USER);
MessageContext context = defaultContext();
bot.promoteAdmin().action().accept(context);
Set<Integer> actual = bot.admins();
Set<Integer> expected = newHashSet(MUSER.id());
Set<Integer> expected = newHashSet(USER.getId());
assertEquals("Could not sudo user", expected, actual);
}
@Test
public void canBanUser() {
addUsers(MUSER);
addUsers(USER);
MessageContext context = defaultContext();
bot.banUser().action().accept(context);
Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(MUSER.id());
Set<Integer> expected = newHashSet(USER.getId());
assertEquals("The ban was not emplaced", expected, actual);
}
@Test
public void canUnbanUser() {
addUsers(MUSER);
bot.blacklist().add(MUSER.id());
addUsers(USER);
bot.blacklist().add(USER.getId());
MessageContext context = defaultContext();
@ -197,55 +198,49 @@ public class AbilityBotTest {
@NotNull
private MessageContext defaultContext() {
MessageContext context = mock(MessageContext.class);
when(context.user()).thenReturn(CREATOR);
when(context.firstArg()).thenReturn(MUSER.username());
MessageContext context = mockContext(CREATOR, GROUP_ID, USER.getUserName());
return context;
}
@Test
public void cannotBanCreator() {
addUsers(MUSER, CREATOR);
MessageContext context = mock(MessageContext.class);
when(context.user()).thenReturn(MUSER);
when(context.firstArg()).thenReturn(CREATOR.username());
addUsers(USER, CREATOR);
MessageContext context = mockContext(USER, GROUP_ID, CREATOR.getUserName());
bot.banUser().action().accept(context);
Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(MUSER.id());
Set<Integer> expected = newHashSet(USER.getId());
assertEquals("Impostor was not added to the blacklist", expected, actual);
}
private void addUsers(EndUser... users) {
private void addUsers(User... users) {
Arrays.stream(users).forEach(user -> {
bot.users().put(user.id(), user);
bot.userIds().put(user.username().toLowerCase(), user.id());
bot.users().put(user.getId(), user);
bot.userIds().put(user.getUserName().toLowerCase(), user.getId());
});
}
@Test
public void creatorCanClaimBot() {
MessageContext context = mock(MessageContext.class);
when(context.user()).thenReturn(CREATOR);
MessageContext context = mockContext(CREATOR, GROUP_ID);
bot.claimCreator().action().accept(context);
Set<Integer> actual = bot.admins();
Set<Integer> expected = newHashSet(CREATOR.id());
Set<Integer> expected = newHashSet(CREATOR.getId());
assertEquals("Creator was not properly added to the super admins set", expected, actual);
}
@Test
public void userGetsBannedIfClaimsBot() {
addUsers(MUSER);
MessageContext context = mock(MessageContext.class);
when(context.user()).thenReturn(MUSER);
addUsers(USER);
MessageContext context = mockContext(USER, GROUP_ID);
bot.claimCreator().action().accept(context);
Set<Integer> actual = bot.blacklist();
Set<Integer> expected = newHashSet(MUSER.id());
Set<Integer> expected = newHashSet(USER.getId());
assertEquals("Could not find user on the blacklist", expected, actual);
actual = bot.admins();
@ -255,7 +250,7 @@ public class AbilityBotTest {
@Test
public void bannedCreatorPassesBlacklistCheck() {
bot.blacklist().add(CREATOR.id());
bot.blacklist().add(CREATOR.getId());
Update update = mock(Update.class);
Message message = mock(Message.class);
User user = mock(User.class);
@ -272,35 +267,35 @@ public class AbilityBotTest {
Message message = mock(Message.class);
User user = mock(User.class);
mockAlternateUser(update, message, user, MUSER);
mockAlternateUser(update, message, USER);
bot.addUser(update);
Map<String, Integer> expectedUserIds = ImmutableMap.of(MUSER.username(), MUSER.id());
Map<Integer, EndUser> expectedUsers = ImmutableMap.of(MUSER.id(), MUSER);
Map<String, Integer> expectedUserIds = ImmutableMap.of(USER.getUserName(), USER.getId());
Map<Integer, User> expectedUsers = ImmutableMap.of(USER.getId(), USER);
assertEquals("User was not added", expectedUserIds, bot.userIds());
assertEquals("User was not added", expectedUsers, bot.users());
}
@Test
public void canEditUser() {
addUsers(MUSER);
addUsers(USER);
Update update = mock(Update.class);
Message message = mock(Message.class);
User user = mock(User.class);
String newUsername = MUSER.username() + "-test";
String newFirstName = MUSER.firstName() + "-test";
String newLastName = MUSER.lastName() + "-test";
int sameId = MUSER.id();
EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername);
String newUsername = USER.getUserName() + "-test";
String newFirstName = USER.getFirstName() + "-test";
String newLastName = USER.getLastName() + "-test";
int sameId = USER.getId();
User changedUser = new User(sameId, newFirstName, false, newLastName, newUsername, null);
mockAlternateUser(update, message, user, changedUser);
mockAlternateUser(update, message, changedUser);
bot.addUser(update);
Map<String, Integer> expectedUserIds = ImmutableMap.of(changedUser.username(), changedUser.id());
Map<Integer, EndUser> expectedUsers = ImmutableMap.of(changedUser.id(), changedUser);
Map<String, Integer> expectedUserIds = ImmutableMap.of(changedUser.getUserName(), changedUser.getId());
Map<Integer, User> expectedUsers = ImmutableMap.of(changedUser.getId(), changedUser);
assertEquals("User was not properly edited", bot.userIds(), expectedUserIds);
assertEquals("User was not properly edited", expectedUsers, expectedUsers);
}
@ -317,7 +312,7 @@ public class AbilityBotTest {
@Test
public void canCheckInput() {
Update update = mockFullUpdate(MUSER, "/something");
Update update = mockFullUpdate(USER, "/something");
Ability abilityWithOneInput = getDefaultBuilder()
.build();
Ability abilityWithZeroInput = getDefaultBuilder()
@ -408,7 +403,7 @@ public class AbilityBotTest {
Trio<Update, Ability, String[]> creatorTrio = Trio.of(update, creatorAbility, TEXT);
bot.admins().add(MUSER.id());
bot.admins().add(USER.getId());
mockUser(update, message, user);
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio));
@ -439,15 +434,14 @@ public class AbilityBotTest {
public void canRetrieveContext() {
Update update = mock(Update.class);
Message message = mock(Message.class);
User user = mock(User.class);
Ability ability = getDefaultBuilder().build();
Trio<Update, Ability, String[]> trio = Trio.of(update, ability, TEXT);
when(message.getChatId()).thenReturn(GROUP_ID);
mockUser(update, message, user);
mockUser(update, message, USER);
Pair<MessageContext, Ability> actualPair = bot.getContext(trio);
Pair<MessageContext, Ability> expectedPair = Pair.of(newContext(update, MUSER, GROUP_ID, TEXT), ability);
Pair<MessageContext, Ability> expectedPair = Pair.of(newContext(update, USER, GROUP_ID, TEXT), ability);
assertEquals("Unexpected result when fetching for context", expectedPair, actualPair);
}
@ -548,18 +542,30 @@ public class AbilityBotTest {
@Test
public void canReportCommands() {
MessageContext context = mockContext(USER, GROUP_ID);
bot.reportCommands().action().accept(context);
verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
}
@NotNull
public static MessageContext mockContext(User user) {
return mockContext(user, user.getId());
}
@NotNull
public static MessageContext mockContext(User user, long groupId, String... args) {
Update update = mock(Update.class);
Message message = mock(Message.class);
when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message);
when(message.getFrom()).thenReturn(user);
when(message.hasText()).thenReturn(true);
MessageContext context = mock(MessageContext.class);
when(context.chatId()).thenReturn(GROUP_ID);
bot.reportCommands().action().accept(context);
verify(silent, times(1)).send("default - dis iz default command", GROUP_ID);
return newContext(update, user, groupId, args);
}
@After
@ -568,26 +574,14 @@ public class AbilityBotTest {
db.close();
}
private User mockUser(EndUser fromUser) {
User user = mock(User.class);
when(user.getId()).thenReturn(fromUser.id());
when(user.getUserName()).thenReturn(fromUser.username());
when(user.getFirstName()).thenReturn(fromUser.firstName());
when(user.getLastName()).thenReturn(fromUser.lastName());
return user;
}
@NotNull
private Update mockFullUpdate(EndUser fromUser, String args) {
bot.users().put(MUSER.id(), MUSER);
bot.users().put(CREATOR.id(), CREATOR);
bot.userIds().put(CREATOR.username(), CREATOR.id());
bot.userIds().put(MUSER.username(), MUSER.id());
private Update mockFullUpdate(User user, String args) {
bot.users().put(USER.getId(), USER);
bot.users().put(CREATOR.getId(), CREATOR);
bot.userIds().put(CREATOR.getUserName(), CREATOR.getId());
bot.userIds().put(USER.getUserName(), USER.getId());
bot.admins().add(CREATOR.id());
User user = mockUser(fromUser);
bot.admins().add(CREATOR.getId());
Update update = mock(Update.class);
when(update.hasMessage()).thenReturn(true);
@ -596,7 +590,7 @@ public class AbilityBotTest {
when(message.getText()).thenReturn(args);
when(message.hasText()).thenReturn(true);
when(message.isUserMessage()).thenReturn(true);
when(message.getChatId()).thenReturn((long) fromUser.id());
when(message.getChatId()).thenReturn((long) user.getId());
when(update.getMessage()).thenReturn(message);
return update;
}
@ -605,17 +599,9 @@ public class AbilityBotTest {
when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message);
when(message.getFrom()).thenReturn(user);
when(user.getFirstName()).thenReturn(MUSER.firstName());
when(user.getLastName()).thenReturn(MUSER.lastName());
when(user.getId()).thenReturn(MUSER.id());
when(user.getUserName()).thenReturn(MUSER.username());
}
private void mockAlternateUser(Update update, Message message, User user, EndUser changedUser) {
when(user.getId()).thenReturn(changedUser.id());
when(user.getFirstName()).thenReturn(changedUser.firstName());
when(user.getLastName()).thenReturn(changedUser.lastName());
when(user.getUserName()).thenReturn(changedUser.username());
private void mockAlternateUser(Update update, Message message, User user) {
when(message.getFrom()).thenReturn(user);
when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message);
@ -627,10 +613,12 @@ public class AbilityBotTest {
Message botMessage = mock(Message.class);
Document document = mock(Document.class);
when(message.getFrom()).thenReturn(CREATOR);
when(update.getMessage()).thenReturn(message);
when(message.getDocument()).thenReturn(document);
when(botMessage.getText()).thenReturn(RECOVERY_MESSAGE);
when(message.isReply()).thenReturn(true);
when(update.hasMessage()).thenReturn(true);
when(message.hasDocument()).thenReturn(true);
when(message.getReplyToMessage()).thenReturn(botMessage);
when(message.getChatId()).thenReturn(GROUP_ID);
@ -652,4 +640,17 @@ 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;
}
}

View File

@ -3,7 +3,7 @@ package org.telegram.abilitybots.api.db;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.telegram.abilitybots.api.objects.EndUser;
import org.telegram.telegrambots.api.objects.User;
import java.io.IOException;
import java.util.Map;
@ -12,12 +12,11 @@ import java.util.Set;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static java.lang.String.format;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.telegram.abilitybots.api.bot.AbilityBot.USERS;
import static org.telegram.abilitybots.api.bot.AbilityBot.USER_ID;
import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR;
import static org.telegram.abilitybots.api.bot.AbilityBotTest.MUSER;
import static org.telegram.abilitybots.api.bot.AbilityBotTest.USER;
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
public class MapDBContextTest {
@ -32,22 +31,22 @@ public class MapDBContextTest {
@Test
public void canRecoverDB() {
Map<Integer, EndUser> users = db.getMap(USERS);
Map<Integer, User> users = db.getMap(USERS);
Map<String, Integer> userIds = db.getMap(USER_ID);
users.put(CREATOR.id(), CREATOR);
users.put(MUSER.id(), MUSER);
userIds.put(CREATOR.username(), CREATOR.id());
userIds.put(MUSER.username(), MUSER.id());
users.put(CREATOR.getId(), CREATOR);
users.put(USER.getId(), USER);
userIds.put(CREATOR.getUserName(), CREATOR.getId());
userIds.put(USER.getUserName(), USER.getId());
db.getSet("AYRE").add(123123);
Map<Integer, EndUser> originalUsers = newHashMap(users);
Map<Integer, User> originalUsers = newHashMap(users);
String beforeBackupInfo = db.info(USERS);
Object jsonBackup = db.backup();
db.clear();
boolean recovered = db.recover(jsonBackup);
Map<Integer, EndUser> recoveredUsers = db.getMap(USERS);
Map<Integer, User> recoveredUsers = db.getMap(USERS);
String afterRecoveryInfo = db.info(USERS);
assertTrue("Could not recover database successfully", recovered);
@ -56,24 +55,24 @@ public class MapDBContextTest {
}
@Test
public void canFallbackDBIfRecoveryFails() throws IOException {
Set<EndUser> users = db.getSet(USERS);
public void canFallbackDBIfRecoveryFails() {
Set<User> users = db.getSet(USERS);
users.add(CREATOR);
users.add(MUSER);
users.add(USER);
Set<EndUser> originalSet = newHashSet(users);
Set<User> originalSet = newHashSet(users);
Object jsonBackup = db.backup();
String corruptBackup = "!@#$" + String.valueOf(jsonBackup);
boolean recovered = db.recover(corruptBackup);
Set<EndUser> recoveredSet = db.getSet(USERS);
Set<User> recoveredSet = db.getSet(USERS);
assertEquals("Recovery was successful from a CORRUPT backup", false, recovered);
assertFalse("Recovery was successful from a CORRUPT backup", recovered);
assertEquals("Set before and after corrupt recovery are not equal", originalSet, recoveredSet);
}
@Test
public void canGetSummary() throws IOException {
public void canGetSummary() {
String anotherTest = TEST + 1;
db.getSet(TEST).add(TEST);
db.getSet(anotherTest).add(anotherTest);
@ -86,7 +85,7 @@ public class MapDBContextTest {
}
@Test
public void canGetInfo() throws IOException {
public void canGetInfo() {
db.getSet(TEST).add(TEST);
String actualInfo = db.info(TEST);
@ -97,7 +96,7 @@ public class MapDBContextTest {
}
@Test(expected = IllegalStateException.class)
public void cantGetInfoFromNonexistentDBStructureName() throws IOException {
public void cantGetInfoFromNonexistentDBStructureName() {
db.info(TEST);
}

View File

@ -0,0 +1 @@
ability.commands.notFound=Non sono presenti comandi pubblici.