From 49cc3fb896cd0b19a92500204617af4a36945774 Mon Sep 17 00:00:00 2001 From: davioooh Date: Wed, 18 Apr 2018 17:14:32 +0200 Subject: [PATCH 01/41] Add basic internationalization support --- .../abilitybots/api/bot/AbilityBot.java | 50 ++++++++++++------- .../abilitybots/api/objects/EndUser.java | 21 ++++++-- .../abilitybots/api/util/AbilityUtils.java | 26 ++++++++++ .../resources/default_messages.properties | 22 ++++++++ .../abilitybots/api/bot/AbilityBotTest.java | 1 + 5 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 telegrambots-abilities/src/main/resources/default_messages.properties diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index a9c4e0e3..48296835 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -103,6 +103,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { protected static final String COMMANDS = "commands"; // Messages + // TODO replace hardcoded 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."; @@ -306,7 +307,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.notFound", ctx.user().locale())); silent.send(commands, ctx.chatId()); }) @@ -371,11 +372,13 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (db.recover(backupData)) { silent.send(RECOVER_SUCCESS, chatId); } else { - silent.send("Oops, something went wrong during recovery.", chatId); + silent.send(getLocalizedMessage("ability.recover.fail", + AbilityUtils.getUser(update).getLanguageCode()), chatId); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - silent.send("I have failed to recover.", chatId); + silent.send(getLocalizedMessage("ability.recover.error", + AbilityUtils.getUser(update).getLanguageCode()), chatId); } }, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE)) .build(); @@ -411,10 +414,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(format("%s is already *banned*.", escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.ban.alreadyBanned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(format("%s is now *banned*.", escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.ban.banned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -439,9 +442,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(format("@%s is *not* on the *blacklist*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.unban.notBanned", ctx.user().locale(), escape(username)), ctx.chatId()); else { - silent.sendMd(format("@%s, your ban has been *lifted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.unban.lifted", ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -463,10 +466,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(format("@%s is already an *admin*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.promote.alreadyPromoted", ctx.user().locale(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(format("@%s has been *promoted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.promote.promoted", ctx.user().locale(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -487,9 +490,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(format("@%s has been *demoted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.demote.demoted", ctx.user().locale(), escape(username)), ctx.chatId()); } else { - silent.sendMd(format("@%s is *not* an *admin*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.demote.alreadyDemoted", ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -514,10 +517,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send("You're already my master.", chatId); + silent.send(getLocalizedMessage("ability.claim.alreadyClaimed", ctx.user().locale()), chatId); else { admins.add(id); - silent.send("You're now my master.", chatId); + silent.send(getLocalizedMessage("ability.claim.claimed", ctx.user().locale()), chatId); } } else { // This is not a joke @@ -615,7 +618,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( + "checkInput.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityTokens, abilityTokens == 1 ? "input" : "inputs"), + getChatId(trio.a())); return isOk; } @@ -627,7 +635,12 @@ 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( + "checkLocality.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityLocality.toString().toLowerCase()), + getChatId(trio.a())); return isOk; } @@ -642,8 +655,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot { 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( + "checkPrivacy.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode()), + getChatId(trio.a())); return isOk; } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 7cc08145..0c123434 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -1,11 +1,14 @@ package org.telegram.abilitybots.api.objects; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import org.telegram.telegrambots.api.objects.User; import java.io.Serializable; +import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; @@ -27,12 +30,15 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; + @JsonIgnore + private Locale locale; - private EndUser(Integer id, String firstName, String lastName, String username) { + private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.username = username; + this.locale = locale != null? locale : Locale.ENGLISH; } @JsonCreator @@ -40,7 +46,7 @@ public final class EndUser implements Serializable { @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName, @JsonProperty("username") String username) { - return new EndUser(id, firstName, lastName, username); + return new EndUser(id, firstName, lastName, username, null); } /** @@ -50,7 +56,8 @@ public final class EndUser implements Serializable { * @return an augmented end-user */ public static EndUser fromUser(User user) { - return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName()); + Locale locale = Strings.isNullOrEmpty(user.getLanguageCode()) ? null : Locale.forLanguageTag(user.getLanguageCode()); + return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName(), locale); } public int id() { @@ -69,6 +76,8 @@ public final class EndUser implements Serializable { return username; } + public Locale locale() { return locale; } + /** * 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. @@ -118,12 +127,13 @@ public final class EndUser implements Serializable { return Objects.equals(id, endUser.id) && Objects.equals(firstName, endUser.firstName) && Objects.equals(lastName, endUser.lastName) && - Objects.equals(username, endUser.username); + Objects.equals(username, endUser.username) && + Objects.equals(locale, endUser.locale); } @Override public int hashCode() { - return Objects.hash(id, firstName, lastName, username); + return Objects.hash(id, firstName, lastName, username, locale); } @Override @@ -133,6 +143,7 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) + .add("locale", locale.toString()) .toString(); } } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index 8852170f..c6d3e554 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -1,10 +1,15 @@ 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.function.Consumer; import java.util.function.Predicate; @@ -150,4 +155,25 @@ public final class AbilityUtils { public static Predicate 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 = ResourceBundle.getBundle("default_messages"); + }else { + try { + bundle = ResourceBundle.getBundle("messages", locale); + } catch (MissingResourceException e) { + bundle = ResourceBundle.getBundle("default_messages"); + } + } + 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); + } + } diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/default_messages.properties new file mode 100644 index 00000000..18dc77df --- /dev/null +++ b/telegrambots-abilities/src/main/resources/default_messages.properties @@ -0,0 +1,22 @@ +ability.commands.notFound=No public commands found. +ability.recover.fail=Oops, something went wrong during recovery. +ability.recover.error=I have failed to recover. + +ability.ban.alreadyBanned={0} is already *banned*. +ability.ban.banned={0} is now *banned*. + +ability.unban.notBanned=@{0} is *not* on the *blacklist*. +ability.unban.lifted=@{0}, your ban has been *lifted*. + +ability.promote.alreadyPromoted=@{0} is already an *admin*. +ability.promote.promoted=@{0} has been *promoted*. + +ability.demote.alreadyDemoted=@{0} is *not* an *admin*. +ability.demote.demoted=@{0} has been *demoted*. + +ability.claim.alreadyClaimed=You''re already my master. +ability.claim.claimed=You''re now 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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index ff335720..ebcd4c54 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -556,6 +556,7 @@ public class AbilityBotTest { when(message.hasText()).thenReturn(true); MessageContext context = mock(MessageContext.class); when(context.chatId()).thenReturn(GROUP_ID); + when(context.user()).thenReturn(MUSER); bot.reportCommands().action().accept(context); From 9f1aa6664fa57f0f74194032d877ad6406e5c57e Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 14:33:10 +0200 Subject: [PATCH 02/41] Update default messages --- .../src/main/resources/default_messages.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/default_messages.properties index 18dc77df..03ae86fc 100644 --- a/telegrambots-abilities/src/main/resources/default_messages.properties +++ b/telegrambots-abilities/src/main/resources/default_messages.properties @@ -1,4 +1,7 @@ ability.commands.notFound=No public commands found. + +ability.recover.message=I am ready to receive the backup file. Please reply to this message with the backup file attached. +ability.recover.success=I have successfully recovered. ability.recover.fail=Oops, something went wrong during recovery. ability.recover.error=I have failed to recover. From 16fa704a17fa35484161cd9598801cbe49770d20 Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 15:39:32 +0200 Subject: [PATCH 03/41] Refactor EndUser to serialize locale --- .../org/telegram/abilitybots/api/objects/EndUser.java | 8 ++++---- .../org/telegram/abilitybots/api/bot/AbilityBotTest.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0c123434..0ea7919d 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -1,7 +1,6 @@ package org.telegram.abilitybots.api.objects; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; @@ -30,7 +29,7 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; - @JsonIgnore + @JsonProperty("locale") private Locale locale; private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { @@ -45,8 +44,9 @@ public final class EndUser implements Serializable { 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, null); + @JsonProperty("username") String username, + @JsonProperty("locale") Locale locale) { + return new EndUser(id, firstName, lastName, username, locale); } /** diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index ebcd4c54..689f1486 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -53,8 +53,8 @@ public class AbilityBotTest { 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 EndUser MUSER = endUser(1, "first", "last", "username", null); + public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); private DefaultBot bot; private DBContext db; @@ -293,7 +293,7 @@ public class AbilityBotTest { String newFirstName = MUSER.firstName() + "-test"; String newLastName = MUSER.lastName() + "-test"; int sameId = MUSER.id(); - EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername); + EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername, null); mockAlternateUser(update, message, user, changedUser); From f64d556610e5aff10bd8fd9d8b38879861f59b9c Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 19:50:44 +0200 Subject: [PATCH 04/41] Refactor localized message helper method --- .../org/telegram/abilitybots/api/util/AbilityUtils.java | 9 ++++++--- .../{default_messages.properties => messages.properties} | 0 2 files changed, 6 insertions(+), 3 deletions(-) rename telegrambots-abilities/src/main/resources/{default_messages.properties => messages.properties} (100%) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index c6d3e554..b64e70bb 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -159,12 +159,15 @@ public final class AbilityUtils { public static String getLocalizedMessage(String messageCode, Locale locale, Object...arguments) { ResourceBundle bundle; if(locale == null){ - bundle = ResourceBundle.getBundle("default_messages"); + bundle = ResourceBundle.getBundle("messages", Locale.ROOT); }else { try { - bundle = ResourceBundle.getBundle("messages", locale); + bundle = ResourceBundle.getBundle( + "messages", + locale, + ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)); } catch (MissingResourceException e) { - bundle = ResourceBundle.getBundle("default_messages"); + bundle = ResourceBundle.getBundle("messages", Locale.ROOT); } } String message = bundle.getString(messageCode); diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/messages.properties similarity index 100% rename from telegrambots-abilities/src/main/resources/default_messages.properties rename to telegrambots-abilities/src/main/resources/messages.properties From 494f21040f5f09c46e05f02ea9ddad159a399f39 Mon Sep 17 00:00:00 2001 From: davioooh Date: Wed, 2 May 2018 11:06:39 +0200 Subject: [PATCH 05/41] Complete externalization of messages --- .../telegram/abilitybots/api/bot/AbilityBot.java | 15 ++++++--------- .../src/main/resources/messages.properties | 2 ++ .../abilitybots/api/bot/AbilityBotTest.java | 6 ++++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 48296835..7ccbce6a 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -102,11 +102,6 @@ public abstract class AbilityBot extends TelegramLongPollingBot { protected static final String RECOVER = "recover"; protected static final String COMMANDS = "commands"; - // Messages - // TODO replace hardcoded 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; @@ -270,7 +265,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try { return getUser(username).id(); } catch (IllegalStateException ex) { - silent.send(format("Sorry, I could not find the user [%s].", username), chatId); + silent.send(getLocalizedMessage("userNotFound","", username), chatId); // TODO how to retrieve language? throw propagate(ex); } } @@ -362,7 +357,8 @@ 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().locale()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -370,7 +366,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try (FileReader reader = new FileReader(downloadFileWithId(fileId))) { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { - silent.send(RECOVER_SUCCESS, chatId); + silent.send(getLocalizedMessage("ability.recover.success", + AbilityUtils.getUser(update).getLanguageCode()), chatId); // TODO how to retrieve language? } else { silent.send(getLocalizedMessage("ability.recover.fail", AbilityUtils.getUser(update).getLanguageCode()), chatId); @@ -380,7 +377,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { silent.send(getLocalizedMessage("ability.recover.error", AbilityUtils.getUser(update).getLanguageCode()), chatId); } - }, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE)) + }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage("ability.recover.success", ""))) // TODO how to retrieve language? .build(); } diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index 03ae86fc..efd03c44 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -23,3 +23,5 @@ ability.claim.claimed=You''re now 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}]. \ No newline at end of file diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 689f1486..51f26353 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -36,8 +36,6 @@ 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; @@ -49,6 +47,10 @@ import static org.telegram.abilitybots.api.objects.MessageContext.newContext; import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { + // 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."; + private static final String[] EMPTY_ARRAY = {}; private static final long GROUP_ID = 10L; private static final String TEST = "test"; From c97476f05dcf12eab35de0d58e0e051070941182 Mon Sep 17 00:00:00 2001 From: davioooh Date: Fri, 4 May 2018 18:15:02 +0200 Subject: [PATCH 06/41] Add basic unit tests --- .../api/bot/AbilityBotI18nTest.java | 76 +++++++++++++++++++ .../api/bot/NoPublicCommandsBot.java | 16 ++++ .../test/resources/messages_it_IT.properties | 1 + 3 files changed, 93 insertions(+) create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java create mode 100644 telegrambots-abilities/src/test/resources/messages_it_IT.properties diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java new file mode 100644 index 00000000..b8799004 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -0,0 +1,76 @@ +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.EndUser; +import org.telegram.abilitybots.api.objects.MessageContext; +import org.telegram.abilitybots.api.sender.MessageSender; +import org.telegram.abilitybots.api.sender.SilentSender; + +import java.io.IOException; +import java.util.Locale; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.mockito.Mockito.*; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; +import static org.telegram.abilitybots.api.objects.EndUser.endUser; + +public class AbilityBotI18nTest { + public static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); + public static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + + private DBContext db; + private DefaultBot bot; + + private NoPublicCommandsBot noCommandsBot; + + private MessageSender sender; + private SilentSender silent; + + @Before + public void setUp() { + db = offlineInstance("db"); + bot = new DefaultBot(EMPTY, EMPTY, db); + + silent = mock(SilentSender.class); + + bot.sender = sender; + bot.silent = silent; + } + + @Test + public void missingPublicCommandsLocalizedCorrectly() { + NoPublicCommandsBot noCommandsBot = new NoPublicCommandsBot(EMPTY, EMPTY, db); + noCommandsBot.silent = silent; + + MessageContext context = mock(MessageContext.class); + when(context.chatId()).thenReturn(Long.valueOf(NO_LANGUAGE_USER.id())); + when(context.user()).thenReturn(NO_LANGUAGE_USER); + + noCommandsBot.reportCommands().action().accept(context); + + verify(silent, times(1)) + .send("No public commands found.", NO_LANGUAGE_USER.id()); + + // + + MessageContext context1 = mock(MessageContext.class); + when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); + when(context1.user()).thenReturn(ITALIAN_USER); + + noCommandsBot.reportCommands().action().accept(context1); + + verify(silent, times(1)) + .send("Non sono presenti comandi pubblici.", ITALIAN_USER.id()); + } + + + @After + public void tearDown() throws IOException { + db.clear(); + db.close(); + } +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java new file mode 100644 index 00000000..fa7b70e5 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java @@ -0,0 +1,16 @@ +package org.telegram.abilitybots.api.bot; + +import org.telegram.abilitybots.api.db.DBContext; + +public class NoPublicCommandsBot extends AbilityBot { + + + protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { + super(botToken, botUsername, db); + } + + @Override + public int creatorId() { + return 0; + } +} \ No newline at end of file diff --git a/telegrambots-abilities/src/test/resources/messages_it_IT.properties b/telegrambots-abilities/src/test/resources/messages_it_IT.properties new file mode 100644 index 00000000..c5656cc1 --- /dev/null +++ b/telegrambots-abilities/src/test/resources/messages_it_IT.properties @@ -0,0 +1 @@ +ability.commands.notFound=Non sono presenti comandi pubblici. \ No newline at end of file From 02fbb67b598d5b2e2c8c9ebabebaa5eb5827c6f0 Mon Sep 17 00:00:00 2001 From: davioooh Date: Fri, 4 May 2018 18:16:04 +0200 Subject: [PATCH 07/41] Remove locale retrieval to avoid exception --- .../main/java/org/telegram/abilitybots/api/bot/AbilityBot.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 7ccbce6a..d4b4cdc9 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -367,7 +367,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { silent.send(getLocalizedMessage("ability.recover.success", - AbilityUtils.getUser(update).getLanguageCode()), chatId); // TODO how to retrieve language? + ""), chatId); + // TODO how to retrieve language? Getting java.lang.IllegalStateException: Could not retrieve originating user from update } else { silent.send(getLocalizedMessage("ability.recover.fail", AbilityUtils.getUser(update).getLanguageCode()), chatId); From 95dd4e3a3f460379905381807b35c16cabbe0ccb Mon Sep 17 00:00:00 2001 From: AzZu Date: Sun, 6 May 2018 11:19:53 +0300 Subject: [PATCH 08/41] remove http proxy feature Cause apache.http package does not provide socks proxy feature --- .../telegrambots/bots/DefaultBotOptions.java | 18 ------------------ .../facilities/TelegramHttpClientBuilder.java | 13 ------------- 2 files changed, 31 deletions(-) diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java index f965ab91..e2e7d25e 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java @@ -23,9 +23,6 @@ public class DefaultBotOptions implements BotOptions { private String baseUrl; private List allowedUpdates; - private CredentialsProvider credentialsProvider; - private HttpHost httpProxy; - public DefaultBotOptions() { maxThreads = 1; baseUrl = ApiConstants.BASE_URL; @@ -88,19 +85,4 @@ public class DefaultBotOptions implements BotOptions { this.exponentialBackOff = exponentialBackOff; } - public CredentialsProvider getCredentialsProvider() { - return credentialsProvider; - } - - public void setCredentialsProvider(CredentialsProvider credentialsProvider) { - this.credentialsProvider = credentialsProvider; - } - - public HttpHost getHttpProxy() { - return httpProxy; - } - - public void setHttpProxy(HttpHost httpProxy) { - this.httpProxy = httpProxy; - } } diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java index fb911c04..57e836a3 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java @@ -18,19 +18,6 @@ public class TelegramHttpClientBuilder { .setSSLHostnameVerifier(new NoopHostnameVerifier()) .setConnectionTimeToLive(70, TimeUnit.SECONDS) .setMaxConnTotal(100); - - if (options.getHttpProxy() != null) { - - httpClientBuilder.setProxy(options.getHttpProxy()); - - if (options.getCredentialsProvider() != null) { - httpClientBuilder - .setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()) - .setDefaultCredentialsProvider(options.getCredentialsProvider()); - } - - } - return httpClientBuilder.build(); } From 346a9dc04597d576a68802b5a57c8280b43ff7a4 Mon Sep 17 00:00:00 2001 From: AzZu Date: Sun, 6 May 2018 20:34:17 +0300 Subject: [PATCH 09/41] added proxy feature So now, it's support http, socks4 and socks5 proxys --- .../telegrambots/bots/DefaultAbsSender.java | 25 ++++++++-- .../telegrambots/bots/DefaultBotOptions.java | 48 ++++++++++++++++++- .../facilities/TelegramHttpClientBuilder.java | 31 ++++++++++++ .../HttpConnectionSocketFactory.java | 33 +++++++++++++ .../HttpSSLConnectionSocketFactory.java | 40 ++++++++++++++++ .../SocksConnectionSocketFactory.java | 37 ++++++++++++++ .../SocksSSLConnectionSocketFactory.java | 43 +++++++++++++++++ .../updatesreceivers/DefaultBotSession.java | 2 +- 8 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpConnectionSocketFactory.java create mode 100644 telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpSSLConnectionSocketFactory.java create mode 100644 telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksConnectionSocketFactory.java create mode 100644 telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksSSLConnectionSocketFactory.java diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java index 400ab4b9..9a2b9452 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java @@ -33,6 +33,7 @@ import org.telegram.telegrambots.updateshandlers.SentCallback; import java.io.IOException; import java.io.Serializable; +import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -56,7 +57,7 @@ public abstract class DefaultAbsSender extends AbsSender { protected final ExecutorService exe; private final ObjectMapper objectMapper = new ObjectMapper(); private final DefaultBotOptions options; - private volatile CloseableHttpClient httpclient; + private volatile CloseableHttpClient httpClient; private volatile RequestConfig requestConfig; protected DefaultAbsSender(DefaultBotOptions options) { @@ -64,10 +65,10 @@ public abstract class DefaultAbsSender extends AbsSender { this.exe = Executors.newFixedThreadPool(options.getMaxThreads()); this.options = options; - httpclient = TelegramHttpClientBuilder.build(options); + httpClient = TelegramHttpClientBuilder.build(options); + configureHttpContext(); requestConfig = options.getRequestConfig(); - if (requestConfig == null) { requestConfig = RequestConfig.copy(RequestConfig.custom().build()) .setSocketTimeout(SOCKET_TIMEOUT) @@ -76,6 +77,22 @@ public abstract class DefaultAbsSender extends AbsSender { } } + private void configureHttpContext() { + + if (options.getProxyType() != DefaultBotOptions.ProxyType.NO_PROXY) { + InetSocketAddress socksaddr = new InetSocketAddress(options.getProxyHost(), options.getProxyPort()); + options.getHttpContext().setAttribute("socketAddress", socksaddr); + } + + if (options.getProxyType() == DefaultBotOptions.ProxyType.SOCKS4) { + options.getHttpContext().setAttribute("socksVersion", 4); + } + if (options.getProxyType() == DefaultBotOptions.ProxyType.SOCKS5) { + options.getHttpContext().setAttribute("socksVersion", 5); + } + + } + /** * Returns the token of the bot to be able to perform Telegram Api Requests * @return Token of the bot @@ -734,7 +751,7 @@ public abstract class DefaultAbsSender extends AbsSender { } private String sendHttpPostRequest(HttpPost httppost) throws IOException { - try (CloseableHttpResponse response = httpclient.execute(httppost)) { + try (CloseableHttpResponse response = httpClient.execute(httppost, options.getHttpContext())) { HttpEntity ht = response.getEntity(); BufferedHttpEntity buf = new BufferedHttpEntity(ht); return EntityUtils.toString(buf, StandardCharsets.UTF_8); diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java index e2e7d25e..95ac515b 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java @@ -1,8 +1,8 @@ package org.telegram.telegrambots.bots; -import org.apache.http.HttpHost; -import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; import org.telegram.telegrambots.ApiConstants; import org.telegram.telegrambots.generics.BotOptions; import org.telegram.telegrambots.updatesreceivers.ExponentialBackOff; @@ -18,14 +18,27 @@ import java.util.List; public class DefaultBotOptions implements BotOptions { private int maxThreads; ///< Max number of threads used for async methods executions (default 1) private RequestConfig requestConfig; + private volatile HttpContext httpContext; private ExponentialBackOff exponentialBackOff; private Integer maxWebhookConnections; private String baseUrl; private List allowedUpdates; + private ProxyType proxyType; + private String proxyHost; + private int proxyPort; + + public enum ProxyType { + NO_PROXY, + HTTP, + SOCKS4, + SOCKS5 + } public DefaultBotOptions() { maxThreads = 1; baseUrl = ApiConstants.BASE_URL; + httpContext = HttpClientContext.create(); + proxyType = ProxyType.NO_PROXY; } @Override @@ -53,6 +66,14 @@ public class DefaultBotOptions implements BotOptions { return maxWebhookConnections; } + public HttpContext getHttpContext() { + return httpContext; + } + + public void setHttpContext(HttpContext httpContext) { + this.httpContext = httpContext; + } + public void setMaxWebhookConnections(Integer maxWebhookConnections) { this.maxWebhookConnections = maxWebhookConnections; } @@ -85,4 +106,27 @@ public class DefaultBotOptions implements BotOptions { this.exponentialBackOff = exponentialBackOff; } + public ProxyType getProxyType() { + return proxyType; + } + + public void setProxyType(ProxyType proxyType) { + this.proxyType = proxyType; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public int getProxyPort() { + return proxyPort; + } + + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } } diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java index 57e836a3..8585460b 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java @@ -1,10 +1,20 @@ package org.telegram.telegrambots.facilities; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.ProxyAuthenticationStrategy; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContexts; import org.telegram.telegrambots.bots.DefaultBotOptions; +import org.telegram.telegrambots.facilities.proxysocketfactorys.HttpConnectionSocketFactory; +import org.telegram.telegrambots.facilities.proxysocketfactorys.HttpSSLConnectionSocketFactory; +import org.telegram.telegrambots.facilities.proxysocketfactorys.SocksSSLConnectionSocketFactory; +import org.telegram.telegrambots.facilities.proxysocketfactorys.SocksConnectionSocketFactory; import java.util.concurrent.TimeUnit; @@ -16,9 +26,30 @@ public class TelegramHttpClientBuilder { public static CloseableHttpClient build(DefaultBotOptions options) { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() .setSSLHostnameVerifier(new NoopHostnameVerifier()) + .setConnectionManager(createConnectionManager(options)) .setConnectionTimeToLive(70, TimeUnit.SECONDS) .setMaxConnTotal(100); return httpClientBuilder.build(); } + private static HttpClientConnectionManager createConnectionManager(DefaultBotOptions options) { + Registry registry; + switch (options.getProxyType()) { + case NO_PROXY: + return null; + case HTTP: + registry = RegistryBuilder. create() + .register("http", new HttpConnectionSocketFactory()) + .register("https", new HttpSSLConnectionSocketFactory(SSLContexts.createSystemDefault())).build(); + return new PoolingHttpClientConnectionManager(registry); + case SOCKS4: + case SOCKS5: + registry = RegistryBuilder. create() + .register("http", new SocksConnectionSocketFactory()) + .register("https", new SocksSSLConnectionSocketFactory(SSLContexts.createSystemDefault())).build(); + return new PoolingHttpClientConnectionManager(registry); + } + return null; + } + } diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpConnectionSocketFactory.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpConnectionSocketFactory.java new file mode 100644 index 00000000..d06f17ce --- /dev/null +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpConnectionSocketFactory.java @@ -0,0 +1,33 @@ +package org.telegram.telegrambots.facilities.proxysocketfactorys; + +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +public class HttpConnectionSocketFactory extends PlainConnectionSocketFactory { + @Override + public Socket createSocket(final HttpContext context) throws IOException { + InetSocketAddress socketAddress = (InetSocketAddress) context.getAttribute("socketAddress"); + Proxy proxy = new Proxy(Proxy.Type.HTTP, socketAddress); + return new Socket(proxy); + } + + @Override + public Socket connectSocket( + int connectTimeout, + Socket socket, + HttpHost host, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + HttpContext context) throws IOException { + String hostName = host.getHostName(); + int port = remoteAddress.getPort(); + InetSocketAddress unresolvedRemote = InetSocketAddress.createUnresolved(hostName, port); + return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); + } +} diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpSSLConnectionSocketFactory.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpSSLConnectionSocketFactory.java new file mode 100644 index 00000000..2c97e934 --- /dev/null +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/HttpSSLConnectionSocketFactory.java @@ -0,0 +1,40 @@ +package org.telegram.telegrambots.facilities.proxysocketfactorys; + +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +public class HttpSSLConnectionSocketFactory extends SSLConnectionSocketFactory { + + public HttpSSLConnectionSocketFactory(final SSLContext sslContext) { + super(sslContext, new NoopHostnameVerifier()); + } + + @Override + public Socket createSocket(final HttpContext context) throws IOException { + InetSocketAddress socketAddress = (InetSocketAddress) context.getAttribute("socketAddress"); + Proxy proxy = new Proxy(Proxy.Type.HTTP, socketAddress); + return new Socket(proxy); + } + + @Override + public Socket connectSocket( + int connectTimeout, + Socket socket, + HttpHost host, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + HttpContext context) throws IOException { + String hostName = host.getHostName(); + int port = remoteAddress.getPort(); + InetSocketAddress unresolvedRemote = InetSocketAddress.createUnresolved(hostName, port); + return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); + } +} diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksConnectionSocketFactory.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksConnectionSocketFactory.java new file mode 100644 index 00000000..d2b29a5e --- /dev/null +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksConnectionSocketFactory.java @@ -0,0 +1,37 @@ +package org.telegram.telegrambots.facilities.proxysocketfactorys; + + +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; +import sun.net.SocksProxy; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +public class SocksConnectionSocketFactory extends PlainConnectionSocketFactory { + + @Override + public Socket createSocket(final HttpContext context) throws IOException { + InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socketAddress"); + int socksVersion = (Integer) context.getAttribute("socksVersion"); + Proxy proxy = SocksProxy.create(socksaddr, socksVersion); + return new Socket(proxy); + } + + @Override + public Socket connectSocket( + int connectTimeout, + Socket socket, + HttpHost host, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + HttpContext context) throws IOException { + String hostName = host.getHostName(); + int port = remoteAddress.getPort(); + InetSocketAddress unresolvedRemote = InetSocketAddress.createUnresolved(hostName, port); + return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); + } +} diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksSSLConnectionSocketFactory.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksSSLConnectionSocketFactory.java new file mode 100644 index 00000000..cbd46b6a --- /dev/null +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/proxysocketfactorys/SocksSSLConnectionSocketFactory.java @@ -0,0 +1,43 @@ +package org.telegram.telegrambots.facilities.proxysocketfactorys; + +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; +import sun.net.SocksProxy; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + + +public class SocksSSLConnectionSocketFactory extends SSLConnectionSocketFactory { + + public SocksSSLConnectionSocketFactory(final SSLContext sslContext) { + super(sslContext, new NoopHostnameVerifier()); + } + + @Override + public Socket createSocket(final HttpContext context) throws IOException { + InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socketAddress"); + int socksVersion = (Integer) context.getAttribute("socksVersion"); + Proxy proxy = SocksProxy.create(socksaddr, socksVersion); + return new Socket(proxy); + } + + @Override + public Socket connectSocket( + int connectTimeout, + Socket socket, + HttpHost host, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + HttpContext context) throws IOException { + String hostName = host.getHostName(); + int port = remoteAddress.getPort(); + InetSocketAddress unresolvedRemote = InetSocketAddress.createUnresolved(hostName, port); + return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); + } +} diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java index 6e6b56b9..37b7fbfe 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java @@ -245,7 +245,7 @@ public class DefaultBotSession implements BotSession { httpPost.setConfig(requestConfig); httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(request), ContentType.APPLICATION_JSON)); - try (CloseableHttpResponse response = httpclient.execute(httpPost)) { + try (CloseableHttpResponse response = httpclient.execute(httpPost, options.getHttpContext())) { HttpEntity ht = response.getEntity(); BufferedHttpEntity buf = new BufferedHttpEntity(ht); String responseContent = EntityUtils.toString(buf, StandardCharsets.UTF_8); From 6087b37483c204f34a0ca2335152b239c1f86987 Mon Sep 17 00:00:00 2001 From: AzZu Date: Sun, 6 May 2018 20:35:42 +0300 Subject: [PATCH 10/41] remove unused imports --- .../java/org/telegram/telegrambots/bots/DefaultAbsSender.java | 4 ---- .../org/telegram/telegrambots/bots/TelegramWebhookBot.java | 2 -- .../telegrambots/facilities/TelegramHttpClientBuilder.java | 1 - .../telegrambots/updatesreceivers/DefaultBotSession.java | 4 ---- 4 files changed, 11 deletions(-) diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java index 9a2b9452..11b69995 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultAbsSender.java @@ -6,14 +6,11 @@ import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.apache.http.util.EntityUtils; import org.telegram.telegrambots.api.methods.BotApiMethod; import org.telegram.telegrambots.api.methods.groupadministration.SetChatPhoto; @@ -40,7 +37,6 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import static org.telegram.telegrambots.Constants.SOCKET_TIMEOUT; diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramWebhookBot.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramWebhookBot.java index 9b2ff0ec..e926ece4 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramWebhookBot.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/TelegramWebhookBot.java @@ -3,12 +3,10 @@ package org.telegram.telegrambots.bots; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java index 8585460b..38a3a17a 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/facilities/TelegramHttpClientBuilder.java @@ -7,7 +7,6 @@ import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContexts; import org.telegram.telegrambots.bots.DefaultBotOptions; diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java index 37b7fbfe..bac2eef3 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java @@ -6,13 +6,10 @@ import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.apache.http.util.EntityUtils; import org.json.JSONException; import org.telegram.telegrambots.ApiConstants; @@ -32,7 +29,6 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidParameterException; import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.TimeUnit; import static org.telegram.telegrambots.Constants.SOCKET_TIMEOUT; From 0c1cb1e05abac27358a45c6b62b6058c66d447d0 Mon Sep 17 00:00:00 2001 From: AzZu Date: Sun, 6 May 2018 21:07:15 +0300 Subject: [PATCH 11/41] update proxy usage page on wiki --- TelegramBots.wiki/Using-Http-Proxy.md | 42 ++++++++++++++------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/TelegramBots.wiki/Using-Http-Proxy.md b/TelegramBots.wiki/Using-Http-Proxy.md index 502f145e..0b7d4c0a 100644 --- a/TelegramBots.wiki/Using-Http-Proxy.md +++ b/TelegramBots.wiki/Using-Http-Proxy.md @@ -31,7 +31,7 @@ public class MyBot extends AbilityBot { Now you are able to set up your proxy -#### without authentication +#### Without authentication ```java public class Main { @@ -51,13 +51,12 @@ public class Main { TelegramBotsApi botsApi = new TelegramBotsApi(); // Set up Http proxy - DefaultBotOptions botOptions = ApiContext.getInstance(DefaultBotOptions.class); + DefaultBotOptions botOptions = ApiContext.getInstance(DefaultBotOptions.class); - HttpHost httpHost = new HttpHost(PROXY_HOST, PROXY_PORT); - - RequestConfig requestConfig = RequestConfig.custom().setProxy(httpHost).setAuthenticationEnabled(false).build(); - botOptions.setRequestConfig(requestConfig); - botOptions.setHttpProxy(httpHost); + botOptions.setProxyHost(PROXY_HOST); + botOptions.setProxyPort(PROXY_PORT); + // Select proxy type: [HTTP|SOCKS4|SOCKS5] (default: NO_PROXY) + botOptions.setProxyType(DefaultBotOptions.ProxyType.SOCKS5); // Register your newly created AbilityBot MyBot bot = new MyBot(BOT_TOKEN, BOT_NAME, botOptions); @@ -89,25 +88,26 @@ public class Main { public static void main(String[] args) { try { + // Create the Authenticator that will return auth's parameters for proxy authentication + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(PROXY_USER, PROXY_PASSWORD.toCharArray()); + } + }); + ApiContextInitializer.init(); // Create the TelegramBotsApi object to register your bots TelegramBotsApi botsApi = new TelegramBotsApi(); // Set up Http proxy - DefaultBotOptions botOptions = ApiContext.getInstance(DefaultBotOptions.class); + DefaultBotOptions botOptions = ApiContext.getInstance(DefaultBotOptions.class); - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials( - new AuthScope(PROXY_HOST, PROXY_PORT), - new UsernamePasswordCredentials(PROXY_USER, PROXY_PASSWORD)); - - HttpHost httpHost = new HttpHost(PROXY_HOST, PROXY_PORT); - - RequestConfig requestConfig = RequestConfig.custom().setProxy(httpHost).setAuthenticationEnabled(true).build(); - botOptions.setRequestConfig(requestConfig); - botOptions.setCredentialsProvider(credsProvider); - botOptions.setHttpProxy(httpHost); + botOptions.setProxyHost(PROXY_HOST); + botOptions.setProxyPort(PROXY_PORT); + // Select proxy type: [HTTP|SOCKS4|SOCKS5] (default: NO_PROXY) + botOptions.setProxyType(DefaultBotOptions.ProxyType.SOCKS5); // Register your newly created AbilityBot MyBot bot = new MyBot(BOT_TOKEN, BOT_NAME, botOptions); @@ -119,4 +119,6 @@ public class Main { } } } -``` \ No newline at end of file +``` + +If you need something more complex than one proxy, then you can create more complex Authenticator that will check host and other parameters of proxy and return auth values based on them (for more information see code of java.net.Authenticator class) From ff73a433375ef98b235e1a527ee3d3b88d296c4d Mon Sep 17 00:00:00 2001 From: AzZu Date: Sun, 6 May 2018 21:13:44 +0300 Subject: [PATCH 12/41] Update version to 3.6.2 Cause new version does not support the old methods of enabling a proxy --- pom.xml | 2 +- telegrambots-abilities/pom.xml | 2 +- telegrambots-extensions/pom.xml | 2 +- telegrambots-meta/pom.xml | 2 +- telegrambots-spring-boot-starter/pom.xml | 2 +- telegrambots/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 4b5ea1dc..9bfeb45f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.telegram Bots pom - 3.6.1 + 3.6.2 telegrambots diff --git a/telegrambots-abilities/pom.xml b/telegrambots-abilities/pom.xml index 716b8bd6..9ccbcd17 100644 --- a/telegrambots-abilities/pom.xml +++ b/telegrambots-abilities/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots-abilities - 3.6.1 + 3.6.2 jar Telegram Ability Bot diff --git a/telegrambots-extensions/pom.xml b/telegrambots-extensions/pom.xml index 35d22cd6..b28a6f6d 100644 --- a/telegrambots-extensions/pom.xml +++ b/telegrambots-extensions/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambotsextensions - 3.6.1 + 3.6.2 jar Telegram Bots Extensions diff --git a/telegrambots-meta/pom.xml b/telegrambots-meta/pom.xml index a37020bc..2e813f79 100644 --- a/telegrambots-meta/pom.xml +++ b/telegrambots-meta/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots-meta - 3.6.1 + 3.6.2 jar Telegram Bots Meta diff --git a/telegrambots-spring-boot-starter/pom.xml b/telegrambots-spring-boot-starter/pom.xml index b40ff9c9..7beb45b6 100644 --- a/telegrambots-spring-boot-starter/pom.xml +++ b/telegrambots-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots-spring-boot-starter - 3.6.1 + 3.6.2 jar Telegram Bots Spring Boot Starter diff --git a/telegrambots/pom.xml b/telegrambots/pom.xml index 95d1f3e3..aceeb9d2 100644 --- a/telegrambots/pom.xml +++ b/telegrambots/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots - 3.6.1 + 3.6.2 jar Telegram Bots From b9da279fdffdb118bb0af323c9df35abc304c1e1 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Tue, 8 May 2018 03:16:20 +0300 Subject: [PATCH 13/41] Remove group update check for GROUP_ADMIN GetChatAdministrators only returns admins when there IS a group, there's no need to explicitly check for groups here. --- .../abilitybots/api/bot/AbilityBot.java | 2 +- .../abilitybots/api/objects/Flag.java | 2 +- .../abilitybots/api/util/AbilityUtils.java | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index a9c4e0e3..bbabe4c3 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -650,7 +650,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { private boolean isGroupAdmin(Update update, int id) { GetChatAdministrators admins = new GetChatAdministrators().setChatId(getChatId(update)); - return isGroupUpdate(update) && silent.execute(admins) + return silent.execute(admins) .orElse(new ArrayList<>()).stream() .anyMatch(member -> member.getUser().getId() == id); } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java index 8b03175e..645646b5 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Flag.java @@ -11,7 +11,7 @@ import static java.util.Objects.nonNull; /** * Flags are an conditions that are applied on an {@link Update}. *

- * They can be used on {@link AbilityBuilder#flag(Flag...)} and on the post conditions in {@link AbilityBuilder#reply(Consumer, Predicate[])}. + * They can be used on {@link AbilityBuilder#flag(Predicate[])} and on the post conditions in {@link AbilityBuilder#reply(Consumer, Predicate[])}. * * @author Abbas Abou Daya */ diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index 8852170f..6e313df4 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -86,6 +86,28 @@ public final class AbilityUtils { } } + /** + * A "best-effort" boolean stating whether the update is a super-group message or not. + * + * @param update a Telegram {@link Update} + * @return whether the update is linked to a group + */ + public static boolean isSuperGroupUpdate(Update update) { + if (MESSAGE.test(update)) { + return update.getMessage().isSuperGroupMessage(); + } else if (CALLBACK_QUERY.test(update)) { + return update.getCallbackQuery().getMessage().isSuperGroupMessage(); + } else if (CHANNEL_POST.test(update)) { + return update.getChannelPost().isSuperGroupMessage(); + } else if (EDITED_CHANNEL_POST.test(update)) { + return update.getEditedChannelPost().isSuperGroupMessage(); + } else if (EDITED_MESSAGE.test(update)) { + return update.getEditedMessage().isSuperGroupMessage(); + } else { + return false; + } + } + /** * Fetches the direct chat ID of the specified update. * From 677b401fc63d60d53cf700d2cbcc684938f6a97b Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Tue, 8 May 2018 03:39:21 +0300 Subject: [PATCH 14/41] Check if the message is a group or super group message for GROUP_ADMIN, #390 --- .../java/org/telegram/abilitybots/api/bot/AbilityBot.java | 2 +- .../java/org/telegram/abilitybots/api/bot/AbilityBotTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index bbabe4c3..c2024f46 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -637,7 +637,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Privacy privacy; int id = user.id(); - privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : 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; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index ff335720..29b26e01 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -362,7 +362,7 @@ public class AbilityBotTest { } @Test - public void canValidateGroupAdminPrivacy() throws TelegramApiException { + public void canValidateGroupAdminPrivacy() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); @@ -383,7 +383,7 @@ public class AbilityBotTest { } @Test - public void canRestrictNormalUsersFromGroupAdminAbilities() throws TelegramApiException { + public void canRestrictNormalUsersFromGroupAdminAbilities() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); From 288a4ab17a09f5820c6e8ed67bcb83b46c28018f Mon Sep 17 00:00:00 2001 From: davioooh Date: Tue, 8 May 2018 11:10:37 +0200 Subject: [PATCH 15/41] Fix issues - export message codes - standardize message codes - format/spacing --- .../abilitybots/api/bot/AbilityBot.java | 41 ++++++++++--------- .../abilitybots/api/objects/EndUser.java | 4 +- .../api/util/AbilityMessageCodes.java | 31 ++++++++++++++ .../abilitybots/api/util/AbilityUtils.java | 15 ++++--- .../src/main/resources/messages.properties | 22 +++++----- .../api/bot/AbilityBotI18nTest.java | 6 +-- .../abilitybots/api/bot/AbilityBotTest.java | 4 +- .../api/bot/NoPublicCommandsBot.java | 1 - 8 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index d4b4cdc9..070a0fb1 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -49,6 +49,7 @@ 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.*; /** @@ -265,7 +266,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try { return getUser(username).id(); } catch (IllegalStateException ex) { - silent.send(getLocalizedMessage("userNotFound","", username), chatId); // TODO how to retrieve language? + silent.send(getLocalizedMessage(USER_NOT_FOUND,"", username), chatId); // TODO how to retrieve language? throw propagate(ex); } } @@ -302,7 +303,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { }) .sorted() .reduce((a, b) -> format("%s%n%s", a, b)) - .orElse(getLocalizedMessage("ability.commands.notFound", ctx.user().locale())); + .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().locale())); silent.send(commands, ctx.chatId()); }) @@ -358,7 +359,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(CREATOR) .input(0) .action(ctx -> silent.forceReply( - getLocalizedMessage("ability.recover.message", ctx.user().locale()), ctx.chatId())) + getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().locale()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -366,19 +367,19 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try (FileReader reader = new FileReader(downloadFileWithId(fileId))) { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { - silent.send(getLocalizedMessage("ability.recover.success", + silent.send(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""), chatId); // TODO how to retrieve language? Getting java.lang.IllegalStateException: Could not retrieve originating user from update } else { - silent.send(getLocalizedMessage("ability.recover.fail", + silent.send(getLocalizedMessage(ABILITY_RECOVER_FAIL, AbilityUtils.getUser(update).getLanguageCode()), chatId); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - silent.send(getLocalizedMessage("ability.recover.error", + silent.send(getLocalizedMessage(ABILITY_RECOVER_ERROR, AbilityUtils.getUser(update).getLanguageCode()), chatId); } - }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage("ability.recover.success", ""))) // TODO how to retrieve language? + }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""))) // TODO how to retrieve language? .build(); } @@ -412,10 +413,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(getLocalizedMessage("ability.ban.alreadyBanned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(getLocalizedMessage("ability.ban.banned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -440,9 +441,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(getLocalizedMessage("ability.unban.notBanned", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); else { - silent.sendMd(getLocalizedMessage("ability.unban.lifted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -464,10 +465,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(getLocalizedMessage("ability.promote.alreadyPromoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(getLocalizedMessage("ability.promote.promoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -488,9 +489,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(getLocalizedMessage("ability.demote.demoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } else { - silent.sendMd(getLocalizedMessage("ability.demote.alreadyDemoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -515,10 +516,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send(getLocalizedMessage("ability.claim.alreadyClaimed", ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, ctx.user().locale()), chatId); else { admins.add(id); - silent.send(getLocalizedMessage("ability.claim.claimed", ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, ctx.user().locale()), chatId); } } else { // This is not a joke @@ -618,7 +619,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkInput.fail", + CHECK_INPUT_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode(), abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a())); @@ -635,7 +636,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkLocality.fail", + CHECK_LOCALITY_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode(), abilityLocality.toString().toLowerCase()), getChatId(trio.a())); @@ -655,7 +656,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkPrivacy.fail", + CHECK_PRIVACY_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode()), getChatId(trio.a())); return isOk; diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0ea7919d..0f65e39b 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -37,7 +37,7 @@ public final class EndUser implements Serializable { this.firstName = firstName; this.lastName = lastName; this.username = username; - this.locale = locale != null? locale : Locale.ENGLISH; + this.locale = locale; } @JsonCreator @@ -143,7 +143,7 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) - .add("locale", locale.toString()) + .add("locale", locale) .toString(); } } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java new file mode 100644 index 00000000..4f21f937 --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java @@ -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"; + +} diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index b64e70bb..973d6ea6 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -13,6 +13,9 @@ import java.util.ResourceBundle; 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.telegram.abilitybots.api.objects.Flag.*; /** @@ -158,16 +161,16 @@ public final class AbilityUtils { public static String getLocalizedMessage(String messageCode, Locale locale, Object...arguments) { ResourceBundle bundle; - if(locale == null){ - bundle = ResourceBundle.getBundle("messages", Locale.ROOT); - }else { + if (locale == null) { + bundle = getBundle("messages", Locale.ROOT); + } else { try { - bundle = ResourceBundle.getBundle( + bundle = getBundle( "messages", locale, - ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)); + getNoFallbackControl(FORMAT_PROPERTIES)); } catch (MissingResourceException e) { - bundle = ResourceBundle.getBundle("messages", Locale.ROOT); + bundle = getBundle("messages", Locale.ROOT); } } String message = bundle.getString(messageCode); diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index efd03c44..a8495e84 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -1,24 +1,24 @@ ability.commands.notFound=No public commands found. -ability.recover.message=I am ready to receive the backup file. Please reply to this message with the backup file attached. 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.alreadyBanned={0} is already *banned*. -ability.ban.banned={0} is now *banned*. +ability.ban.success={0} is now *banned*. +ability.ban.fail={0} is already *banned*. -ability.unban.notBanned=@{0} is *not* on the *blacklist*. -ability.unban.lifted=@{0}, your ban has been *lifted*. +ability.unban.success=@{0}, your ban has been *lifted*. +ability.unban.fail=@{0} is *not* on the *blacklist*. -ability.promote.alreadyPromoted=@{0} is already an *admin*. -ability.promote.promoted=@{0} has been *promoted*. +ability.promote.success=@{0} has been *promoted*. +ability.promote.fail=@{0} is already an *admin*. -ability.demote.alreadyDemoted=@{0} is *not* an *admin*. -ability.demote.demoted=@{0} has been *demoted*. +ability.demote.success=@{0} has been *demoted*. +ability.demote.fail=@{0} is *not* an *admin*. -ability.claim.alreadyClaimed=You''re already my master. -ability.claim.claimed=You''re now my master. +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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index b8799004..d64cfe14 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -19,8 +19,8 @@ import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - public static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); - public static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); + private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); private DBContext db; private DefaultBot bot; @@ -55,8 +55,6 @@ public class AbilityBotI18nTest { verify(silent, times(1)) .send("No public commands found.", NO_LANGUAGE_USER.id()); - // - MessageContext context1 = mock(MessageContext.class); when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); when(context1.user()).thenReturn(ITALIAN_USER); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 51f26353..dd896295 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -48,8 +48,8 @@ import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { // 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."; + 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; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java index fa7b70e5..b4e49973 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java @@ -4,7 +4,6 @@ import org.telegram.abilitybots.api.db.DBContext; public class NoPublicCommandsBot extends AbilityBot { - protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { super(botToken, botUsername, db); } From 9a436910ff4d179f849e29391dc0d46a9212746c Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Thu, 10 May 2018 01:27:57 +0300 Subject: [PATCH 16/41] Add detailed constructor, equals and hashCode to User object --- .../telegrambots/api/objects/User.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/User.java b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/User.java index 88a0f0a9..828a2cf3 100644 --- a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/User.java +++ b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/User.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.telegram.telegrambots.api.interfaces.BotApiObject; +import java.util.Objects; + /** * @author Ruben Bermudez * @version 3.0 @@ -35,6 +37,15 @@ public class User implements BotApiObject { super(); } + public User(Integer id, String firstName, Boolean isBot, String lastName, String userName, String languageCode) { + this.id = id; + this.firstName = firstName; + this.isBot = isBot; + this.lastName = lastName; + this.userName = userName; + this.languageCode = languageCode; + } + public Integer getId() { return id; } @@ -59,6 +70,24 @@ public class User implements BotApiObject { return isBot; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id) && + Objects.equals(firstName, user.firstName) && + Objects.equals(isBot, user.isBot) && + Objects.equals(lastName, user.lastName) && + Objects.equals(userName, user.userName) && + Objects.equals(languageCode, user.languageCode); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, isBot, lastName, userName, languageCode); + } + @Override public String toString() { return "User{" + From dcc4f29ddd54ed497e15770135fde6ba8324e45f Mon Sep 17 00:00:00 2001 From: zhaoyi Date: Sat, 12 May 2018 14:52:57 +0800 Subject: [PATCH 17/41] Make longPollingBots or webHookBots in spring boot configuration --- .../TelegramBotStarterConfiguration.java | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java index ec851dc0..0d7058e9 100644 --- a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java +++ b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java @@ -1,23 +1,24 @@ package org.telegram.telegrambots.starter; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.TelegramBotsApi; -import org.telegram.telegrambots.exceptions.TelegramApiException; +import org.telegram.telegrambots.exceptions.TelegramApiRequestException; import org.telegram.telegrambots.generics.LongPollingBot; import org.telegram.telegrambots.generics.WebhookBot; +import javax.annotation.PostConstruct; import java.util.List; +import java.util.Optional; /** * Receives all beand which are #LongPollingBot and #WebhookBot and register them in #TelegramBotsApi. * #TelegramBotsApi added to spring context as well */ @Configuration -public class TelegramBotStarterConfiguration implements CommandLineRunner { +public class TelegramBotStarterConfiguration { private final List longPollingBots; @@ -26,24 +27,34 @@ public class TelegramBotStarterConfiguration implements CommandLineRunner { @Autowired private TelegramBotsApi telegramBotsApi; - public TelegramBotStarterConfiguration(List longPollingBots, - List webHookBots) { + public TelegramBotStarterConfiguration(@Autowired(required = false) List longPollingBots, + @Autowired(required = false) List webHookBots) { + this.longPollingBots = longPollingBots; this.webHookBots = webHookBots; } - @Override - public void run(String... args) { - try { - for (LongPollingBot bot : longPollingBots) { - telegramBotsApi.registerBot(bot); - } - for (WebhookBot bot : webHookBots) { - telegramBotsApi.registerBot(bot); - } - } catch (TelegramApiException e) { - e.printStackTrace(); - } + @PostConstruct + public void registerBots() { + Optional.ofNullable(longPollingBots).ifPresent(bots -> { + bots.forEach(bot -> { + try { + telegramBotsApi.registerBot(bot); + } catch (TelegramApiRequestException e) { + throw new RuntimeException(e); + } + }); + }); + + Optional.ofNullable(webHookBots).ifPresent(bots -> { + bots.forEach(bot -> { + try { + telegramBotsApi.registerBot(bot); + } catch (TelegramApiRequestException e) { + throw new RuntimeException(e); + } + }); + }); } From 31f5c64058bcf0fc4baa48e62c1f9e41b9e28fc5 Mon Sep 17 00:00:00 2001 From: davioooh Date: Sun, 13 May 2018 11:16:51 +0200 Subject: [PATCH 18/41] Refactor localized messages retrievement --- .../abilitybots/api/bot/AbilityBot.java | 24 +++++++++---------- .../abilitybots/api/objects/EndUser.java | 23 +++++------------- .../api/bot/AbilityBotI18nTest.java | 5 ++-- .../abilitybots/api/bot/AbilityBotTest.java | 6 ++--- 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 070a0fb1..0d905f20 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -303,7 +303,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { }) .sorted() .reduce((a, b) -> format("%s%n%s", a, b)) - .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().locale())); + .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, AbilityUtils.getUser(ctx.update()).getLanguageCode())); silent.send(commands, ctx.chatId()); }) @@ -359,7 +359,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(CREATOR) .input(0) .action(ctx -> silent.forceReply( - getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().locale()), ctx.chatId())) + getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(ctx.update()).getLanguageCode()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -413,10 +413,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -441,9 +441,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); else { - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -465,10 +465,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -489,9 +489,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } else { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -516,10 +516,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode()), chatId); else { admins.add(id); - silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode()), chatId); } } else { // This is not a joke diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0f65e39b..7cc08145 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -3,11 +3,9 @@ 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 com.google.common.base.Strings; import org.telegram.telegrambots.api.objects.User; import java.io.Serializable; -import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; @@ -29,24 +27,20 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; - @JsonProperty("locale") - private Locale locale; - private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { + private EndUser(Integer id, String firstName, String lastName, String username) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.username = username; - this.locale = locale; } @JsonCreator public static EndUser endUser(@JsonProperty("id") Integer id, @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName, - @JsonProperty("username") String username, - @JsonProperty("locale") Locale locale) { - return new EndUser(id, firstName, lastName, username, locale); + @JsonProperty("username") String username) { + return new EndUser(id, firstName, lastName, username); } /** @@ -56,8 +50,7 @@ public final class EndUser implements Serializable { * @return an augmented end-user */ public static EndUser fromUser(User user) { - Locale locale = Strings.isNullOrEmpty(user.getLanguageCode()) ? null : Locale.forLanguageTag(user.getLanguageCode()); - return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName(), locale); + return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName()); } public int id() { @@ -76,8 +69,6 @@ public final class EndUser implements Serializable { return username; } - public Locale locale() { return locale; } - /** * 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. @@ -127,13 +118,12 @@ public final class EndUser implements Serializable { return Objects.equals(id, endUser.id) && Objects.equals(firstName, endUser.firstName) && Objects.equals(lastName, endUser.lastName) && - Objects.equals(username, endUser.username) && - Objects.equals(locale, endUser.locale); + Objects.equals(username, endUser.username); } @Override public int hashCode() { - return Objects.hash(id, firstName, lastName, username, locale); + return Objects.hash(id, firstName, lastName, username); } @Override @@ -143,7 +133,6 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) - .add("locale", locale) .toString(); } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index d64cfe14..68b178e1 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -10,7 +10,6 @@ import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender; import java.io.IOException; -import java.util.Locale; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.*; @@ -19,8 +18,8 @@ import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); - private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username"); + private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username"); private DBContext db; private DefaultBot bot; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index dd896295..3d958717 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -55,8 +55,8 @@ public class AbilityBotTest { 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", null); - public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); + public static final EndUser MUSER = endUser(1, "first", "last", "username"); + public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername"); private DefaultBot bot; private DBContext db; @@ -295,7 +295,7 @@ public class AbilityBotTest { String newFirstName = MUSER.firstName() + "-test"; String newLastName = MUSER.lastName() + "-test"; int sameId = MUSER.id(); - EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername, null); + EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername); mockAlternateUser(update, message, user, changedUser); From d0651e60b4e3e82d1478ef8b308a2f89f35785f5 Mon Sep 17 00:00:00 2001 From: zhaoyi Date: Mon, 14 May 2018 15:17:07 +0800 Subject: [PATCH 19/41] Add unit test for spring boot start configuration --- telegrambots-spring-boot-starter/pom.xml | 13 ++++ .../TelegramBotStarterConfiguration.java | 6 +- .../TestTelegramBotStarterConfiguration.java | 66 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java diff --git a/telegrambots-spring-boot-starter/pom.xml b/telegrambots-spring-boot-starter/pom.xml index b40ff9c9..72ed3705 100644 --- a/telegrambots-spring-boot-starter/pom.xml +++ b/telegrambots-spring-boot-starter/pom.xml @@ -80,6 +80,19 @@ spring-boot-autoconfigure ${spring-boot.version} + + + org.mockito + mockito-all + 2.0.2-beta + test + + + junit + junit + 4.11 + test + diff --git a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java index 0d7058e9..11e66af3 100644 --- a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java +++ b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java @@ -24,9 +24,13 @@ public class TelegramBotStarterConfiguration { private final List longPollingBots; private final List webHookBots; - @Autowired private TelegramBotsApi telegramBotsApi; + @Autowired + public void setTelegramBotsApi(TelegramBotsApi telegramBotsApi) { + this.telegramBotsApi = telegramBotsApi; + } + public TelegramBotStarterConfiguration(@Autowired(required = false) List longPollingBots, @Autowired(required = false) List webHookBots) { diff --git a/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java b/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java new file mode 100644 index 00000000..00a1e7cf --- /dev/null +++ b/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java @@ -0,0 +1,66 @@ +package org.telegram.telegrambots.starter; + +import com.google.common.collect.Lists; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.telegram.telegrambots.TelegramBotsApi; +import org.telegram.telegrambots.exceptions.TelegramApiRequestException; +import org.telegram.telegrambots.generics.LongPollingBot; +import org.telegram.telegrambots.generics.WebhookBot; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +public class TestTelegramBotStarterConfiguration { + + @Mock + private TelegramBotsApi telegramBotsApi; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Test + public void TestRegisterBotsWithLongPollingBots() throws TelegramApiRequestException { + when(telegramBotsApi.registerBot(any(LongPollingBot.class))).then(Answers.RETURNS_MOCKS.get()); + LongPollingBot longPollingBot = mock(LongPollingBot.class); + TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(Lists.newArrayList(longPollingBot), null); + configuration.setTelegramBotsApi(telegramBotsApi); + + configuration.registerBots(); + + verify(telegramBotsApi, times(1)).registerBot(longPollingBot); + verifyNoMoreInteractions(telegramBotsApi); + } + + @Test + public void TestRegisterBotsWithWebhookBots() throws TelegramApiRequestException { + doNothing().when(telegramBotsApi).registerBot(any(WebhookBot.class)); + WebhookBot webhookBot = mock(WebhookBot.class); + TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(null, Lists.newArrayList(webhookBot)); + configuration.setTelegramBotsApi(telegramBotsApi); + + configuration.registerBots(); + + verify(telegramBotsApi, times(1)).registerBot(webhookBot); + verifyNoMoreInteractions(telegramBotsApi); + } + + @Test + public void TestRegisterBotsWithLongPollingBotsAndWebhookBots() throws TelegramApiRequestException { + doNothing().when(telegramBotsApi).registerBot(any(WebhookBot.class)); + LongPollingBot longPollingBot = mock(LongPollingBot.class); + WebhookBot webhookBot = mock(WebhookBot.class); + TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(Lists.newArrayList(longPollingBot), Lists.newArrayList(webhookBot)); + configuration.setTelegramBotsApi(telegramBotsApi); + + configuration.registerBots(); + + verify(telegramBotsApi, times(1)).registerBot(longPollingBot); + verify(telegramBotsApi, times(1)).registerBot(webhookBot); + verifyNoMoreInteractions(telegramBotsApi); + } +} From 9357883cf31b1389d7d1e079b0337d8879ff0e1d Mon Sep 17 00:00:00 2001 From: Alexey Zhohov Date: Thu, 17 May 2018 17:21:55 +0300 Subject: [PATCH 20/41] fix field name pending_update_count of WebhookInfo --- .../org/telegram/telegrambots/api/objects/WebhookInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/WebhookInfo.java b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/WebhookInfo.java index 6a61a8f1..a25aacc1 100644 --- a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/WebhookInfo.java +++ b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/WebhookInfo.java @@ -16,7 +16,7 @@ public class WebhookInfo implements BotApiObject { private static final String URL_FIELD = "url"; private static final String HASCUSTOMCERTIFICATE_FIELD = "has_custom_certificate"; - private static final String PENDINGUPDATESCOUNT_FIELD = "pending_updates_count"; + private static final String PENDINGUPDATECOUNT_FIELD = "pending_update_count"; private static final String MAXCONNECTIONS_FIELD = "max_connections"; private static final String ALLOWEDUPDATES_FIELD = "allowed_updates"; private static final String LASTERRORDATE_FIELD = "last_error_date"; @@ -26,7 +26,7 @@ public class WebhookInfo implements BotApiObject { private String url; ///< Webhook URL, may be empty if webhook is not set up @JsonProperty(HASCUSTOMCERTIFICATE_FIELD) private Boolean hasCustomCertificate; ///< True, if a custom certificate was provided for webhook certificate checks - @JsonProperty(PENDINGUPDATESCOUNT_FIELD) + @JsonProperty(PENDINGUPDATECOUNT_FIELD) private Integer pendingUpdatesCount; ///< Number updates awaiting delivery @JsonProperty(LASTERRORDATE_FIELD) private Integer lastErrorDate; ///< Optional. Unix time for the most recent error that happened when trying to deliver an update via webhook From c713f9da48a82f2ef766619786bf53154d0c0319 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Fri, 18 May 2018 04:40:27 -0400 Subject: [PATCH 21/41] Fix tests --- .../api/bot/AbilityBotI18nTest.java | 91 ++++++++++--------- .../abilitybots/api/bot/AbilityBotTest.java | 63 +++++++++---- .../api/bot/NoPublicCommandsBot.java | 15 --- 3 files changed, 96 insertions(+), 73 deletions(-) delete mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 68b178e1..75aa6df0 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -4,70 +4,79 @@ 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.EndUser; 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 java.lang.Long.valueOf; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.telegram.abilitybots.api.bot.AbilityBotTest.mockContext; +import static org.telegram.abilitybots.api.bot.AbilityBotTest.newUser; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; -import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username"); - private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username"); + private static final User NO_LANGUAGE_USER = newUser(1, "first", "last", "username", null); + private static final User ITALIAN_USER = newUser(2, "first", "last", "username", "it-IT"); - private DBContext db; - private DefaultBot bot; + private DBContext db; + private NoPublicCommandsBot bot; - private NoPublicCommandsBot noCommandsBot; + private MessageSender sender; + private SilentSender silent; - private MessageSender sender; - private SilentSender silent; + @Before + public void setUp() { + db = offlineInstance("db"); + bot = new NoPublicCommandsBot(EMPTY, EMPTY, db); - @Before - public void setUp() { - db = offlineInstance("db"); - bot = new DefaultBot(EMPTY, EMPTY, db); + sender = mock(MessageSender.class); + silent = mock(SilentSender.class); - silent = mock(SilentSender.class); + bot.sender = sender; + bot.silent = silent; + } - bot.sender = sender; - bot.silent = silent; - } + @Test + public void missingPublicCommandsLocalizedCorrectly1() { + MessageContext context = mockContext(NO_LANGUAGE_USER); - @Test - public void missingPublicCommandsLocalizedCorrectly() { - NoPublicCommandsBot noCommandsBot = new NoPublicCommandsBot(EMPTY, EMPTY, db); - noCommandsBot.silent = silent; + bot.reportCommands().action().accept(context); - MessageContext context = mock(MessageContext.class); - when(context.chatId()).thenReturn(Long.valueOf(NO_LANGUAGE_USER.id())); - when(context.user()).thenReturn(NO_LANGUAGE_USER); + verify(silent, times(1)) + .send("No public commands found.", NO_LANGUAGE_USER.getId()); + } - noCommandsBot.reportCommands().action().accept(context); + @Test + public void missingPublicCommandsLocalizedCorrectly2() { + MessageContext context1 = mockContext(ITALIAN_USER); - verify(silent, times(1)) - .send("No public commands found.", NO_LANGUAGE_USER.id()); + bot.reportCommands().action().accept(context1); - MessageContext context1 = mock(MessageContext.class); - when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); - when(context1.user()).thenReturn(ITALIAN_USER); - - noCommandsBot.reportCommands().action().accept(context1); - - verify(silent, times(1)) - .send("Non sono presenti comandi pubblici.", ITALIAN_USER.id()); - } + verify(silent, times(1)) + .send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId()); + } - @After - public void tearDown() throws IOException { - db.clear(); - db.close(); - } + @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 0; + } + } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 3d958717..2e03ad6a 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -39,6 +39,7 @@ import static org.mockito.internal.verification.VerificationModeFactory.times; 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.EndUser.fromUser; 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; @@ -56,7 +57,9 @@ public class AbilityBotTest { 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 User TG_USER = newUser(1, "first", "last", "username", null); public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername"); + public static final User TG_CREATOR = newUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); private DefaultBot bot; private DBContext db; @@ -199,8 +202,7 @@ public class AbilityBotTest { @NotNull private MessageContext defaultContext() { - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(CREATOR); + MessageContext context = mockContext(TG_CREATOR, GROUP_ID); when(context.firstArg()).thenReturn(MUSER.username()); return context; } @@ -208,8 +210,7 @@ public class AbilityBotTest { @Test public void cannotBanCreator() { addUsers(MUSER, CREATOR); - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_USER, GROUP_ID); when(context.firstArg()).thenReturn(CREATOR.username()); bot.banUser().action().accept(context); @@ -228,8 +229,7 @@ public class AbilityBotTest { @Test public void creatorCanClaimBot() { - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(CREATOR); + MessageContext context = mockContext(TG_CREATOR, GROUP_ID); bot.claimCreator().action().accept(context); @@ -241,8 +241,7 @@ public class AbilityBotTest { @Test public void userGetsBannedIfClaimsBot() { addUsers(MUSER); - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_USER, GROUP_ID); bot.claimCreator().action().accept(context); @@ -550,21 +549,38 @@ public class AbilityBotTest { @Test public void canReportCommands() { - 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 context = mock(MessageContext.class); - when(context.chatId()).thenReturn(GROUP_ID); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_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) { + Update update = mock(Update.class); + Message message = mock(Message.class); + EndUser endUser = fromUser(user); + + 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.update()).thenReturn(update); + when(context.chatId()).thenReturn(groupId); + when(context.user()).thenReturn(endUser); + + return context; + } + @After public void tearDown() throws IOException { db.clear(); @@ -655,4 +671,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; + } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java deleted file mode 100644 index b4e49973..00000000 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.telegram.abilitybots.api.bot; - -import org.telegram.abilitybots.api.db.DBContext; - -public class NoPublicCommandsBot extends AbilityBot { - - protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { - super(botToken, botUsername, db); - } - - @Override - public int creatorId() { - return 0; - } -} \ No newline at end of file From d77887fd2c4b6456225aa420131220c7750082d1 Mon Sep 17 00:00:00 2001 From: davioooh Date: Wed, 18 Apr 2018 17:14:32 +0200 Subject: [PATCH 22/41] Add basic internationalization support --- .../abilitybots/api/bot/AbilityBot.java | 50 ++++++++++++------- .../abilitybots/api/objects/EndUser.java | 21 ++++++-- .../abilitybots/api/util/AbilityUtils.java | 26 ++++++++++ .../resources/default_messages.properties | 22 ++++++++ .../abilitybots/api/bot/AbilityBotTest.java | 1 + 5 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 telegrambots-abilities/src/main/resources/default_messages.properties diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index c2024f46..fd4d4aae 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -103,6 +103,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { protected static final String COMMANDS = "commands"; // Messages + // TODO replace hardcoded 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."; @@ -306,7 +307,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.notFound", ctx.user().locale())); silent.send(commands, ctx.chatId()); }) @@ -371,11 +372,13 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (db.recover(backupData)) { silent.send(RECOVER_SUCCESS, chatId); } else { - silent.send("Oops, something went wrong during recovery.", chatId); + silent.send(getLocalizedMessage("ability.recover.fail", + AbilityUtils.getUser(update).getLanguageCode()), chatId); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - silent.send("I have failed to recover.", chatId); + silent.send(getLocalizedMessage("ability.recover.error", + AbilityUtils.getUser(update).getLanguageCode()), chatId); } }, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE)) .build(); @@ -411,10 +414,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(format("%s is already *banned*.", escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.ban.alreadyBanned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(format("%s is now *banned*.", escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.ban.banned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -439,9 +442,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(format("@%s is *not* on the *blacklist*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.unban.notBanned", ctx.user().locale(), escape(username)), ctx.chatId()); else { - silent.sendMd(format("@%s, your ban has been *lifted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.unban.lifted", ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -463,10 +466,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(format("@%s is already an *admin*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.promote.alreadyPromoted", ctx.user().locale(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(format("@%s has been *promoted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.promote.promoted", ctx.user().locale(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -487,9 +490,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(format("@%s has been *demoted*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.demote.demoted", ctx.user().locale(), escape(username)), ctx.chatId()); } else { - silent.sendMd(format("@%s is *not* an *admin*.", escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage("ability.demote.alreadyDemoted", ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -514,10 +517,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send("You're already my master.", chatId); + silent.send(getLocalizedMessage("ability.claim.alreadyClaimed", ctx.user().locale()), chatId); else { admins.add(id); - silent.send("You're now my master.", chatId); + silent.send(getLocalizedMessage("ability.claim.claimed", ctx.user().locale()), chatId); } } else { // This is not a joke @@ -615,7 +618,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( + "checkInput.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityTokens, abilityTokens == 1 ? "input" : "inputs"), + getChatId(trio.a())); return isOk; } @@ -627,7 +635,12 @@ 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( + "checkLocality.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityLocality.toString().toLowerCase()), + getChatId(trio.a())); return isOk; } @@ -642,8 +655,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot { 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( + "checkPrivacy.fail", + AbilityUtils.getUser(trio.a()).getLanguageCode()), + getChatId(trio.a())); return isOk; } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 7cc08145..0c123434 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -1,11 +1,14 @@ package org.telegram.abilitybots.api.objects; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import org.telegram.telegrambots.api.objects.User; import java.io.Serializable; +import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; @@ -27,12 +30,15 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; + @JsonIgnore + private Locale locale; - private EndUser(Integer id, String firstName, String lastName, String username) { + private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.username = username; + this.locale = locale != null? locale : Locale.ENGLISH; } @JsonCreator @@ -40,7 +46,7 @@ public final class EndUser implements Serializable { @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName, @JsonProperty("username") String username) { - return new EndUser(id, firstName, lastName, username); + return new EndUser(id, firstName, lastName, username, null); } /** @@ -50,7 +56,8 @@ public final class EndUser implements Serializable { * @return an augmented end-user */ public static EndUser fromUser(User user) { - return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName()); + Locale locale = Strings.isNullOrEmpty(user.getLanguageCode()) ? null : Locale.forLanguageTag(user.getLanguageCode()); + return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName(), locale); } public int id() { @@ -69,6 +76,8 @@ public final class EndUser implements Serializable { return username; } + public Locale locale() { return locale; } + /** * 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. @@ -118,12 +127,13 @@ public final class EndUser implements Serializable { return Objects.equals(id, endUser.id) && Objects.equals(firstName, endUser.firstName) && Objects.equals(lastName, endUser.lastName) && - Objects.equals(username, endUser.username); + Objects.equals(username, endUser.username) && + Objects.equals(locale, endUser.locale); } @Override public int hashCode() { - return Objects.hash(id, firstName, lastName, username); + return Objects.hash(id, firstName, lastName, username, locale); } @Override @@ -133,6 +143,7 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) + .add("locale", locale.toString()) .toString(); } } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index 6e313df4..21043fb3 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -1,10 +1,15 @@ 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.function.Consumer; import java.util.function.Predicate; @@ -172,4 +177,25 @@ public final class AbilityUtils { public static Predicate 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 = ResourceBundle.getBundle("default_messages"); + }else { + try { + bundle = ResourceBundle.getBundle("messages", locale); + } catch (MissingResourceException e) { + bundle = ResourceBundle.getBundle("default_messages"); + } + } + 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); + } + } diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/default_messages.properties new file mode 100644 index 00000000..18dc77df --- /dev/null +++ b/telegrambots-abilities/src/main/resources/default_messages.properties @@ -0,0 +1,22 @@ +ability.commands.notFound=No public commands found. +ability.recover.fail=Oops, something went wrong during recovery. +ability.recover.error=I have failed to recover. + +ability.ban.alreadyBanned={0} is already *banned*. +ability.ban.banned={0} is now *banned*. + +ability.unban.notBanned=@{0} is *not* on the *blacklist*. +ability.unban.lifted=@{0}, your ban has been *lifted*. + +ability.promote.alreadyPromoted=@{0} is already an *admin*. +ability.promote.promoted=@{0} has been *promoted*. + +ability.demote.alreadyDemoted=@{0} is *not* an *admin*. +ability.demote.demoted=@{0} has been *demoted*. + +ability.claim.alreadyClaimed=You''re already my master. +ability.claim.claimed=You''re now 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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 29b26e01..8727e86f 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -556,6 +556,7 @@ public class AbilityBotTest { when(message.hasText()).thenReturn(true); MessageContext context = mock(MessageContext.class); when(context.chatId()).thenReturn(GROUP_ID); + when(context.user()).thenReturn(MUSER); bot.reportCommands().action().accept(context); From 45ea5af1271e06eaaebba6d6b271c59a52bf0a53 Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 14:33:10 +0200 Subject: [PATCH 23/41] Update default messages --- .../src/main/resources/default_messages.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/default_messages.properties index 18dc77df..03ae86fc 100644 --- a/telegrambots-abilities/src/main/resources/default_messages.properties +++ b/telegrambots-abilities/src/main/resources/default_messages.properties @@ -1,4 +1,7 @@ ability.commands.notFound=No public commands found. + +ability.recover.message=I am ready to receive the backup file. Please reply to this message with the backup file attached. +ability.recover.success=I have successfully recovered. ability.recover.fail=Oops, something went wrong during recovery. ability.recover.error=I have failed to recover. From 97a4c0031b4f837205b0fba09544c3ea8f01bf93 Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 15:39:32 +0200 Subject: [PATCH 24/41] Refactor EndUser to serialize locale --- .../org/telegram/abilitybots/api/objects/EndUser.java | 8 ++++---- .../org/telegram/abilitybots/api/bot/AbilityBotTest.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0c123434..0ea7919d 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -1,7 +1,6 @@ package org.telegram.abilitybots.api.objects; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; @@ -30,7 +29,7 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; - @JsonIgnore + @JsonProperty("locale") private Locale locale; private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { @@ -45,8 +44,9 @@ public final class EndUser implements Serializable { 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, null); + @JsonProperty("username") String username, + @JsonProperty("locale") Locale locale) { + return new EndUser(id, firstName, lastName, username, locale); } /** diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 8727e86f..b66f28ef 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -53,8 +53,8 @@ public class AbilityBotTest { 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 EndUser MUSER = endUser(1, "first", "last", "username", null); + public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); private DefaultBot bot; private DBContext db; @@ -293,7 +293,7 @@ public class AbilityBotTest { String newFirstName = MUSER.firstName() + "-test"; String newLastName = MUSER.lastName() + "-test"; int sameId = MUSER.id(); - EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername); + EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername, null); mockAlternateUser(update, message, user, changedUser); From 7ff5be3a72bdb56bbe503933c875fc0db644505e Mon Sep 17 00:00:00 2001 From: davioooh Date: Mon, 30 Apr 2018 19:50:44 +0200 Subject: [PATCH 25/41] Refactor localized message helper method --- .../org/telegram/abilitybots/api/util/AbilityUtils.java | 9 ++++++--- .../{default_messages.properties => messages.properties} | 0 2 files changed, 6 insertions(+), 3 deletions(-) rename telegrambots-abilities/src/main/resources/{default_messages.properties => messages.properties} (100%) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index 21043fb3..a213e43b 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -181,12 +181,15 @@ public final class AbilityUtils { public static String getLocalizedMessage(String messageCode, Locale locale, Object...arguments) { ResourceBundle bundle; if(locale == null){ - bundle = ResourceBundle.getBundle("default_messages"); + bundle = ResourceBundle.getBundle("messages", Locale.ROOT); }else { try { - bundle = ResourceBundle.getBundle("messages", locale); + bundle = ResourceBundle.getBundle( + "messages", + locale, + ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)); } catch (MissingResourceException e) { - bundle = ResourceBundle.getBundle("default_messages"); + bundle = ResourceBundle.getBundle("messages", Locale.ROOT); } } String message = bundle.getString(messageCode); diff --git a/telegrambots-abilities/src/main/resources/default_messages.properties b/telegrambots-abilities/src/main/resources/messages.properties similarity index 100% rename from telegrambots-abilities/src/main/resources/default_messages.properties rename to telegrambots-abilities/src/main/resources/messages.properties From c8f1c69fb2841c71a8afae8fff654db0ccf44e54 Mon Sep 17 00:00:00 2001 From: davioooh Date: Wed, 2 May 2018 11:06:39 +0200 Subject: [PATCH 26/41] Complete externalization of messages --- .../telegram/abilitybots/api/bot/AbilityBot.java | 15 ++++++--------- .../src/main/resources/messages.properties | 2 ++ .../abilitybots/api/bot/AbilityBotTest.java | 6 ++++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index fd4d4aae..44e99b0a 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -102,11 +102,6 @@ public abstract class AbilityBot extends TelegramLongPollingBot { protected static final String RECOVER = "recover"; protected static final String COMMANDS = "commands"; - // Messages - // TODO replace hardcoded 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; @@ -270,7 +265,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try { return getUser(username).id(); } catch (IllegalStateException ex) { - silent.send(format("Sorry, I could not find the user [%s].", username), chatId); + silent.send(getLocalizedMessage("userNotFound","", username), chatId); // TODO how to retrieve language? throw propagate(ex); } } @@ -362,7 +357,8 @@ 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().locale()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -370,7 +366,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try (FileReader reader = new FileReader(downloadFileWithId(fileId))) { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { - silent.send(RECOVER_SUCCESS, chatId); + silent.send(getLocalizedMessage("ability.recover.success", + AbilityUtils.getUser(update).getLanguageCode()), chatId); // TODO how to retrieve language? } else { silent.send(getLocalizedMessage("ability.recover.fail", AbilityUtils.getUser(update).getLanguageCode()), chatId); @@ -380,7 +377,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { silent.send(getLocalizedMessage("ability.recover.error", AbilityUtils.getUser(update).getLanguageCode()), chatId); } - }, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE)) + }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage("ability.recover.success", ""))) // TODO how to retrieve language? .build(); } diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index 03ae86fc..efd03c44 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -23,3 +23,5 @@ ability.claim.claimed=You''re now 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}]. \ No newline at end of file diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index b66f28ef..5dda6847 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -36,8 +36,6 @@ 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; @@ -49,6 +47,10 @@ import static org.telegram.abilitybots.api.objects.MessageContext.newContext; import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { + // 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."; + private static final String[] EMPTY_ARRAY = {}; private static final long GROUP_ID = 10L; private static final String TEST = "test"; From aecfe7693a42ffc9d0b9ffde2583e725e09ed1c6 Mon Sep 17 00:00:00 2001 From: davioooh Date: Fri, 4 May 2018 18:15:02 +0200 Subject: [PATCH 27/41] Add basic unit tests --- .../api/bot/AbilityBotI18nTest.java | 76 +++++++++++++++++++ .../api/bot/NoPublicCommandsBot.java | 16 ++++ .../test/resources/messages_it_IT.properties | 1 + 3 files changed, 93 insertions(+) create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java create mode 100644 telegrambots-abilities/src/test/resources/messages_it_IT.properties diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java new file mode 100644 index 00000000..b8799004 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -0,0 +1,76 @@ +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.EndUser; +import org.telegram.abilitybots.api.objects.MessageContext; +import org.telegram.abilitybots.api.sender.MessageSender; +import org.telegram.abilitybots.api.sender.SilentSender; + +import java.io.IOException; +import java.util.Locale; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.mockito.Mockito.*; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; +import static org.telegram.abilitybots.api.objects.EndUser.endUser; + +public class AbilityBotI18nTest { + public static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); + public static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + + private DBContext db; + private DefaultBot bot; + + private NoPublicCommandsBot noCommandsBot; + + private MessageSender sender; + private SilentSender silent; + + @Before + public void setUp() { + db = offlineInstance("db"); + bot = new DefaultBot(EMPTY, EMPTY, db); + + silent = mock(SilentSender.class); + + bot.sender = sender; + bot.silent = silent; + } + + @Test + public void missingPublicCommandsLocalizedCorrectly() { + NoPublicCommandsBot noCommandsBot = new NoPublicCommandsBot(EMPTY, EMPTY, db); + noCommandsBot.silent = silent; + + MessageContext context = mock(MessageContext.class); + when(context.chatId()).thenReturn(Long.valueOf(NO_LANGUAGE_USER.id())); + when(context.user()).thenReturn(NO_LANGUAGE_USER); + + noCommandsBot.reportCommands().action().accept(context); + + verify(silent, times(1)) + .send("No public commands found.", NO_LANGUAGE_USER.id()); + + // + + MessageContext context1 = mock(MessageContext.class); + when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); + when(context1.user()).thenReturn(ITALIAN_USER); + + noCommandsBot.reportCommands().action().accept(context1); + + verify(silent, times(1)) + .send("Non sono presenti comandi pubblici.", ITALIAN_USER.id()); + } + + + @After + public void tearDown() throws IOException { + db.clear(); + db.close(); + } +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java new file mode 100644 index 00000000..fa7b70e5 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java @@ -0,0 +1,16 @@ +package org.telegram.abilitybots.api.bot; + +import org.telegram.abilitybots.api.db.DBContext; + +public class NoPublicCommandsBot extends AbilityBot { + + + protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { + super(botToken, botUsername, db); + } + + @Override + public int creatorId() { + return 0; + } +} \ No newline at end of file diff --git a/telegrambots-abilities/src/test/resources/messages_it_IT.properties b/telegrambots-abilities/src/test/resources/messages_it_IT.properties new file mode 100644 index 00000000..c5656cc1 --- /dev/null +++ b/telegrambots-abilities/src/test/resources/messages_it_IT.properties @@ -0,0 +1 @@ +ability.commands.notFound=Non sono presenti comandi pubblici. \ No newline at end of file From bd7092921a3d506e10569d30f4f9ed082bbc92af Mon Sep 17 00:00:00 2001 From: davioooh Date: Fri, 4 May 2018 18:16:04 +0200 Subject: [PATCH 28/41] Remove locale retrieval to avoid exception --- .../main/java/org/telegram/abilitybots/api/bot/AbilityBot.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 44e99b0a..298b91b4 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -367,7 +367,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { silent.send(getLocalizedMessage("ability.recover.success", - AbilityUtils.getUser(update).getLanguageCode()), chatId); // TODO how to retrieve language? + ""), chatId); + // TODO how to retrieve language? Getting java.lang.IllegalStateException: Could not retrieve originating user from update } else { silent.send(getLocalizedMessage("ability.recover.fail", AbilityUtils.getUser(update).getLanguageCode()), chatId); From ab86947cc33cd842359d6500187ce84206b2a566 Mon Sep 17 00:00:00 2001 From: davioooh Date: Tue, 8 May 2018 11:10:37 +0200 Subject: [PATCH 29/41] Fix issues - export message codes - standardize message codes - format/spacing --- .../abilitybots/api/bot/AbilityBot.java | 41 ++++++++++--------- .../abilitybots/api/objects/EndUser.java | 4 +- .../api/util/AbilityMessageCodes.java | 31 ++++++++++++++ .../abilitybots/api/util/AbilityUtils.java | 15 ++++--- .../src/main/resources/messages.properties | 22 +++++----- .../api/bot/AbilityBotI18nTest.java | 6 +-- .../abilitybots/api/bot/AbilityBotTest.java | 4 +- .../api/bot/NoPublicCommandsBot.java | 1 - 8 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 298b91b4..b67b9760 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -49,6 +49,7 @@ 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.*; /** @@ -265,7 +266,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try { return getUser(username).id(); } catch (IllegalStateException ex) { - silent.send(getLocalizedMessage("userNotFound","", username), chatId); // TODO how to retrieve language? + silent.send(getLocalizedMessage(USER_NOT_FOUND,"", username), chatId); // TODO how to retrieve language? throw propagate(ex); } } @@ -302,7 +303,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { }) .sorted() .reduce((a, b) -> format("%s%n%s", a, b)) - .orElse(getLocalizedMessage("ability.commands.notFound", ctx.user().locale())); + .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().locale())); silent.send(commands, ctx.chatId()); }) @@ -358,7 +359,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(CREATOR) .input(0) .action(ctx -> silent.forceReply( - getLocalizedMessage("ability.recover.message", ctx.user().locale()), ctx.chatId())) + getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().locale()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -366,19 +367,19 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try (FileReader reader = new FileReader(downloadFileWithId(fileId))) { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { - silent.send(getLocalizedMessage("ability.recover.success", + silent.send(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""), chatId); // TODO how to retrieve language? Getting java.lang.IllegalStateException: Could not retrieve originating user from update } else { - silent.send(getLocalizedMessage("ability.recover.fail", + silent.send(getLocalizedMessage(ABILITY_RECOVER_FAIL, AbilityUtils.getUser(update).getLanguageCode()), chatId); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - silent.send(getLocalizedMessage("ability.recover.error", + silent.send(getLocalizedMessage(ABILITY_RECOVER_ERROR, AbilityUtils.getUser(update).getLanguageCode()), chatId); } - }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage("ability.recover.success", ""))) // TODO how to retrieve language? + }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""))) // TODO how to retrieve language? .build(); } @@ -412,10 +413,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(getLocalizedMessage("ability.ban.alreadyBanned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(getLocalizedMessage("ability.ban.banned", ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -440,9 +441,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(getLocalizedMessage("ability.unban.notBanned", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); else { - silent.sendMd(getLocalizedMessage("ability.unban.lifted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -464,10 +465,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(getLocalizedMessage("ability.promote.alreadyPromoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(getLocalizedMessage("ability.promote.promoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -488,9 +489,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(getLocalizedMessage("ability.demote.demoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); } else { - silent.sendMd(getLocalizedMessage("ability.demote.alreadyDemoted", ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -515,10 +516,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send(getLocalizedMessage("ability.claim.alreadyClaimed", ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, ctx.user().locale()), chatId); else { admins.add(id); - silent.send(getLocalizedMessage("ability.claim.claimed", ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, ctx.user().locale()), chatId); } } else { // This is not a joke @@ -618,7 +619,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkInput.fail", + CHECK_INPUT_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode(), abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a())); @@ -635,7 +636,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkLocality.fail", + CHECK_LOCALITY_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode(), abilityLocality.toString().toLowerCase()), getChatId(trio.a())); @@ -655,7 +656,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( getLocalizedMessage( - "checkPrivacy.fail", + CHECK_PRIVACY_FAIL, AbilityUtils.getUser(trio.a()).getLanguageCode()), getChatId(trio.a())); return isOk; diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0ea7919d..0f65e39b 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -37,7 +37,7 @@ public final class EndUser implements Serializable { this.firstName = firstName; this.lastName = lastName; this.username = username; - this.locale = locale != null? locale : Locale.ENGLISH; + this.locale = locale; } @JsonCreator @@ -143,7 +143,7 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) - .add("locale", locale.toString()) + .add("locale", locale) .toString(); } } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java new file mode 100644 index 00000000..4f21f937 --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityMessageCodes.java @@ -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"; + +} diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index a213e43b..81209885 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -13,6 +13,9 @@ import java.util.ResourceBundle; 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.telegram.abilitybots.api.objects.Flag.*; /** @@ -180,16 +183,16 @@ public final class AbilityUtils { public static String getLocalizedMessage(String messageCode, Locale locale, Object...arguments) { ResourceBundle bundle; - if(locale == null){ - bundle = ResourceBundle.getBundle("messages", Locale.ROOT); - }else { + if (locale == null) { + bundle = getBundle("messages", Locale.ROOT); + } else { try { - bundle = ResourceBundle.getBundle( + bundle = getBundle( "messages", locale, - ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)); + getNoFallbackControl(FORMAT_PROPERTIES)); } catch (MissingResourceException e) { - bundle = ResourceBundle.getBundle("messages", Locale.ROOT); + bundle = getBundle("messages", Locale.ROOT); } } String message = bundle.getString(messageCode); diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index efd03c44..a8495e84 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -1,24 +1,24 @@ ability.commands.notFound=No public commands found. -ability.recover.message=I am ready to receive the backup file. Please reply to this message with the backup file attached. 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.alreadyBanned={0} is already *banned*. -ability.ban.banned={0} is now *banned*. +ability.ban.success={0} is now *banned*. +ability.ban.fail={0} is already *banned*. -ability.unban.notBanned=@{0} is *not* on the *blacklist*. -ability.unban.lifted=@{0}, your ban has been *lifted*. +ability.unban.success=@{0}, your ban has been *lifted*. +ability.unban.fail=@{0} is *not* on the *blacklist*. -ability.promote.alreadyPromoted=@{0} is already an *admin*. -ability.promote.promoted=@{0} has been *promoted*. +ability.promote.success=@{0} has been *promoted*. +ability.promote.fail=@{0} is already an *admin*. -ability.demote.alreadyDemoted=@{0} is *not* an *admin*. -ability.demote.demoted=@{0} has been *demoted*. +ability.demote.success=@{0} has been *demoted*. +ability.demote.fail=@{0} is *not* an *admin*. -ability.claim.alreadyClaimed=You''re already my master. -ability.claim.claimed=You''re now my master. +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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index b8799004..d64cfe14 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -19,8 +19,8 @@ import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - public static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); - public static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); + private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); private DBContext db; private DefaultBot bot; @@ -55,8 +55,6 @@ public class AbilityBotI18nTest { verify(silent, times(1)) .send("No public commands found.", NO_LANGUAGE_USER.id()); - // - MessageContext context1 = mock(MessageContext.class); when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); when(context1.user()).thenReturn(ITALIAN_USER); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 5dda6847..9fc8f99d 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -48,8 +48,8 @@ import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { // 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."; + 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; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java index fa7b70e5..b4e49973 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java @@ -4,7 +4,6 @@ import org.telegram.abilitybots.api.db.DBContext; public class NoPublicCommandsBot extends AbilityBot { - protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { super(botToken, botUsername, db); } From 9347d064c1e32907759aaba53367a7a7a1ec000d Mon Sep 17 00:00:00 2001 From: davioooh Date: Sun, 13 May 2018 11:16:51 +0200 Subject: [PATCH 30/41] Refactor localized messages retrievement --- .../abilitybots/api/bot/AbilityBot.java | 24 +++++++++---------- .../abilitybots/api/objects/EndUser.java | 23 +++++------------- .../api/bot/AbilityBotI18nTest.java | 5 ++-- .../abilitybots/api/bot/AbilityBotTest.java | 6 ++--- 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index b67b9760..4c25a9d8 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -303,7 +303,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { }) .sorted() .reduce((a, b) -> format("%s%n%s", a, b)) - .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().locale())); + .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, AbilityUtils.getUser(ctx.update()).getLanguageCode())); silent.send(commands, ctx.chatId()); }) @@ -359,7 +359,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(CREATOR) .input(0) .action(ctx -> silent.forceReply( - getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().locale()), ctx.chatId())) + getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(ctx.update()).getLanguageCode()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -413,10 +413,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); else { blacklist.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, ctx.user().locale(), escape(bannedUser)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); } }) .post(commitTo(db)) @@ -441,9 +441,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); else { - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -465,10 +465,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); else { admins.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }).post(commitTo(db)) .build(); @@ -489,9 +489,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } else { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, ctx.user().locale(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -516,10 +516,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode()), chatId); else { admins.add(id); - silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, ctx.user().locale()), chatId); + silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode()), chatId); } } else { // This is not a joke diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java index 0f65e39b..7cc08145 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java @@ -3,11 +3,9 @@ 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 com.google.common.base.Strings; import org.telegram.telegrambots.api.objects.User; import java.io.Serializable; -import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; @@ -29,24 +27,20 @@ public final class EndUser implements Serializable { private final String lastName; @JsonProperty("username") private final String username; - @JsonProperty("locale") - private Locale locale; - private EndUser(Integer id, String firstName, String lastName, String username, Locale locale) { + private EndUser(Integer id, String firstName, String lastName, String username) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.username = username; - this.locale = locale; } @JsonCreator public static EndUser endUser(@JsonProperty("id") Integer id, @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName, - @JsonProperty("username") String username, - @JsonProperty("locale") Locale locale) { - return new EndUser(id, firstName, lastName, username, locale); + @JsonProperty("username") String username) { + return new EndUser(id, firstName, lastName, username); } /** @@ -56,8 +50,7 @@ public final class EndUser implements Serializable { * @return an augmented end-user */ public static EndUser fromUser(User user) { - Locale locale = Strings.isNullOrEmpty(user.getLanguageCode()) ? null : Locale.forLanguageTag(user.getLanguageCode()); - return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName(), locale); + return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName()); } public int id() { @@ -76,8 +69,6 @@ public final class EndUser implements Serializable { return username; } - public Locale locale() { return locale; } - /** * 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. @@ -127,13 +118,12 @@ public final class EndUser implements Serializable { return Objects.equals(id, endUser.id) && Objects.equals(firstName, endUser.firstName) && Objects.equals(lastName, endUser.lastName) && - Objects.equals(username, endUser.username) && - Objects.equals(locale, endUser.locale); + Objects.equals(username, endUser.username); } @Override public int hashCode() { - return Objects.hash(id, firstName, lastName, username, locale); + return Objects.hash(id, firstName, lastName, username); } @Override @@ -143,7 +133,6 @@ public final class EndUser implements Serializable { .add("firstName", firstName) .add("lastName", lastName) .add("username", username) - .add("locale", locale) .toString(); } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index d64cfe14..68b178e1 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -10,7 +10,6 @@ import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender; import java.io.IOException; -import java.util.Locale; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.*; @@ -19,8 +18,8 @@ import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username", null); - private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username", Locale.ITALY); + private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username"); + private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username"); private DBContext db; private DefaultBot bot; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 9fc8f99d..3d1f7ae3 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -55,8 +55,8 @@ public class AbilityBotTest { 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", null); - public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); + public static final EndUser MUSER = endUser(1, "first", "last", "username"); + public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername"); private DefaultBot bot; private DBContext db; @@ -295,7 +295,7 @@ public class AbilityBotTest { String newFirstName = MUSER.firstName() + "-test"; String newLastName = MUSER.lastName() + "-test"; int sameId = MUSER.id(); - EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername, null); + EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername); mockAlternateUser(update, message, user, changedUser); From 8729271d34c9521e14e42b3f8931e52941261c81 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Fri, 18 May 2018 04:40:27 -0400 Subject: [PATCH 31/41] Fix tests --- .../api/bot/AbilityBotI18nTest.java | 91 ++++++++++--------- .../abilitybots/api/bot/AbilityBotTest.java | 63 +++++++++---- .../api/bot/NoPublicCommandsBot.java | 15 --- 3 files changed, 96 insertions(+), 73 deletions(-) delete mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 68b178e1..75aa6df0 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -4,70 +4,79 @@ 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.EndUser; 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 java.lang.Long.valueOf; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.telegram.abilitybots.api.bot.AbilityBotTest.mockContext; +import static org.telegram.abilitybots.api.bot.AbilityBotTest.newUser; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; -import static org.telegram.abilitybots.api.objects.EndUser.endUser; public class AbilityBotI18nTest { - private static final EndUser NO_LANGUAGE_USER = endUser(1, "first", "last", "username"); - private static final EndUser ITALIAN_USER = endUser(2, "first", "last", "username"); + private static final User NO_LANGUAGE_USER = newUser(1, "first", "last", "username", null); + private static final User ITALIAN_USER = newUser(2, "first", "last", "username", "it-IT"); - private DBContext db; - private DefaultBot bot; + private DBContext db; + private NoPublicCommandsBot bot; - private NoPublicCommandsBot noCommandsBot; + private MessageSender sender; + private SilentSender silent; - private MessageSender sender; - private SilentSender silent; + @Before + public void setUp() { + db = offlineInstance("db"); + bot = new NoPublicCommandsBot(EMPTY, EMPTY, db); - @Before - public void setUp() { - db = offlineInstance("db"); - bot = new DefaultBot(EMPTY, EMPTY, db); + sender = mock(MessageSender.class); + silent = mock(SilentSender.class); - silent = mock(SilentSender.class); + bot.sender = sender; + bot.silent = silent; + } - bot.sender = sender; - bot.silent = silent; - } + @Test + public void missingPublicCommandsLocalizedCorrectly1() { + MessageContext context = mockContext(NO_LANGUAGE_USER); - @Test - public void missingPublicCommandsLocalizedCorrectly() { - NoPublicCommandsBot noCommandsBot = new NoPublicCommandsBot(EMPTY, EMPTY, db); - noCommandsBot.silent = silent; + bot.reportCommands().action().accept(context); - MessageContext context = mock(MessageContext.class); - when(context.chatId()).thenReturn(Long.valueOf(NO_LANGUAGE_USER.id())); - when(context.user()).thenReturn(NO_LANGUAGE_USER); + verify(silent, times(1)) + .send("No public commands found.", NO_LANGUAGE_USER.getId()); + } - noCommandsBot.reportCommands().action().accept(context); + @Test + public void missingPublicCommandsLocalizedCorrectly2() { + MessageContext context1 = mockContext(ITALIAN_USER); - verify(silent, times(1)) - .send("No public commands found.", NO_LANGUAGE_USER.id()); + bot.reportCommands().action().accept(context1); - MessageContext context1 = mock(MessageContext.class); - when(context1.chatId()).thenReturn(Long.valueOf(ITALIAN_USER.id())); - when(context1.user()).thenReturn(ITALIAN_USER); - - noCommandsBot.reportCommands().action().accept(context1); - - verify(silent, times(1)) - .send("Non sono presenti comandi pubblici.", ITALIAN_USER.id()); - } + verify(silent, times(1)) + .send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId()); + } - @After - public void tearDown() throws IOException { - db.clear(); - db.close(); - } + @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 0; + } + } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 3d1f7ae3..54e48308 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -39,6 +39,7 @@ import static org.mockito.internal.verification.VerificationModeFactory.times; 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.EndUser.fromUser; 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; @@ -56,7 +57,9 @@ public class AbilityBotTest { 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 User TG_USER = newUser(1, "first", "last", "username", null); public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername"); + public static final User TG_CREATOR = newUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); private DefaultBot bot; private DBContext db; @@ -199,8 +202,7 @@ public class AbilityBotTest { @NotNull private MessageContext defaultContext() { - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(CREATOR); + MessageContext context = mockContext(TG_CREATOR, GROUP_ID); when(context.firstArg()).thenReturn(MUSER.username()); return context; } @@ -208,8 +210,7 @@ public class AbilityBotTest { @Test public void cannotBanCreator() { addUsers(MUSER, CREATOR); - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_USER, GROUP_ID); when(context.firstArg()).thenReturn(CREATOR.username()); bot.banUser().action().accept(context); @@ -228,8 +229,7 @@ public class AbilityBotTest { @Test public void creatorCanClaimBot() { - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(CREATOR); + MessageContext context = mockContext(TG_CREATOR, GROUP_ID); bot.claimCreator().action().accept(context); @@ -241,8 +241,7 @@ public class AbilityBotTest { @Test public void userGetsBannedIfClaimsBot() { addUsers(MUSER); - MessageContext context = mock(MessageContext.class); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_USER, GROUP_ID); bot.claimCreator().action().accept(context); @@ -550,21 +549,38 @@ public class AbilityBotTest { @Test public void canReportCommands() { - 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 context = mock(MessageContext.class); - when(context.chatId()).thenReturn(GROUP_ID); - when(context.user()).thenReturn(MUSER); + MessageContext context = mockContext(TG_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) { + Update update = mock(Update.class); + Message message = mock(Message.class); + EndUser endUser = fromUser(user); + + 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.update()).thenReturn(update); + when(context.chatId()).thenReturn(groupId); + when(context.user()).thenReturn(endUser); + + return context; + } + @After public void tearDown() throws IOException { db.clear(); @@ -655,4 +671,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; + } } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java deleted file mode 100644 index b4e49973..00000000 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/NoPublicCommandsBot.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.telegram.abilitybots.api.bot; - -import org.telegram.abilitybots.api.db.DBContext; - -public class NoPublicCommandsBot extends AbilityBot { - - protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { - super(botToken, botUsername, db); - } - - @Override - public int creatorId() { - return 0; - } -} \ No newline at end of file From a819d7f1786b3e8e3a4a8b9e8f9c864162f31a2d Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 21 May 2018 07:01:45 -0400 Subject: [PATCH 32/41] Cleanup and refactoring --- .../abilitybots/api/bot/AbilityBot.java | 126 ++++++++------- .../abilitybots/api/objects/EndUser.java | 138 ---------------- .../api/objects/MessageContext.java | 9 +- .../abilitybots/api/objects/Reply.java | 2 +- .../abilitybots/api/util/AbilityUtils.java | 42 ++++- .../api/bot/AbilityBotI18nTest.java | 10 +- .../abilitybots/api/bot/AbilityBotTest.java | 151 +++++++----------- .../abilitybots/api/db/MapDBContextTest.java | 39 +++-- 8 files changed, 197 insertions(+), 320 deletions(-) delete mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 4c25a9d8..4b0c03fd 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -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,7 +45,6 @@ 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; @@ -145,9 +145,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { } /** - * @return the map of ID -> EndUser + * @return the map of ID -> User */ - protected Map users() { + protected Map users() { return db.getMap(USERS); } @@ -232,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)); @@ -247,13 +247,13 @@ 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; } /** @@ -264,9 +264,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { */ protected int getUserIdSendError(String username, long chatId) { try { - return getUser(username).id(); + return getUser(username).getId(); } catch (IllegalStateException ex) { - silent.send(getLocalizedMessage(USER_NOT_FOUND,"", username), chatId); // TODO how to retrieve language? + silent.send(getLocalizedMessage(USER_NOT_FOUND, "", username), chatId); // TODO how to retrieve language? throw propagate(ex); } } @@ -303,7 +303,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { }) .sorted() .reduce((a, b) -> format("%s%n%s", a, b)) - .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, AbilityUtils.getUser(ctx.update()).getLanguageCode())); + .orElse(getLocalizedMessage(ABILITY_COMMANDS_NOT_FOUND, ctx.user().getLanguageCode())); silent.send(commands, ctx.chatId()); }) @@ -359,7 +359,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(CREATOR) .input(0) .action(ctx -> silent.forceReply( - getLocalizedMessage(ABILITY_RECOVER_MESSAGE, AbilityUtils.getUser(ctx.update()).getLanguageCode()), ctx.chatId())) + getLocalizedMessage(ABILITY_RECOVER_MESSAGE, ctx.user().getLanguageCode()), ctx.chatId())) .reply(update -> { Long chatId = update.getMessage().getChatId(); String fileId = update.getMessage().getDocument().getFileId(); @@ -367,17 +367,13 @@ public abstract class AbilityBot extends TelegramLongPollingBot { try (FileReader reader = new FileReader(downloadFileWithId(fileId))) { String backupData = IOUtils.toString(reader); if (db.recover(backupData)) { - silent.send(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, - ""), chatId); - // TODO how to retrieve language? Getting java.lang.IllegalStateException: Could not retrieve originating user from update + send(ABILITY_RECOVER_SUCCESS, update, chatId); } else { - silent.send(getLocalizedMessage(ABILITY_RECOVER_FAIL, - AbilityUtils.getUser(update).getLanguageCode()), chatId); + send(ABILITY_RECOVER_FAIL, update, chatId); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - silent.send(getLocalizedMessage(ABILITY_RECOVER_ERROR, - AbilityUtils.getUser(update).getLanguageCode()), chatId); + send(ABILITY_RECOVER_ERROR, update, chatId); } }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""))) // TODO how to retrieve language? .build(); @@ -405,18 +401,18 @@ public abstract class AbilityBot extends TelegramLongPollingBot { // 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 blacklist = blacklist(); if (blacklist.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_BAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); + sendMd(ABILITY_BAN_FAIL, ctx, escape(bannedUser)); else { blacklist.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_BAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(bannedUser)), ctx.chatId()); + sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser)); } }) .post(commitTo(db)) @@ -441,9 +437,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set blacklist = blacklist(); if (!blacklist.remove(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_FAIL, ctx.user().getLanguageCode(), escape(username)), ctx.chatId()); else { - silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId()); } }) .post(commitTo(db)) @@ -465,10 +461,10 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.contains(userId)) - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + sendMd(ABILITY_PROMOTE_FAIL, ctx, escape(username)); else { admins.add(userId); - silent.sendMd(getLocalizedMessage(ABILITY_PROMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username)); } }).post(commitTo(db)) .build(); @@ -489,9 +485,9 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Set admins = admins(); if (admins.remove(userId)) { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + sendMd(ABILITY_DEMOTE_SUCCESS, ctx, escape(username)); } else { - silent.sendMd(getLocalizedMessage(ABILITY_DEMOTE_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode(), escape(username)), ctx.chatId()); + sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username)); } }) .post(commitTo(db)) @@ -510,26 +506,37 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .privacy(PUBLIC) .input(0) .action(ctx -> { - if (ctx.user().id() == creatorId()) { + if (ctx.user().getId() == creatorId()) { Set admins = admins(); int id = creatorId(); - long chatId = ctx.chatId(); if (admins.contains(id)) - silent.send(getLocalizedMessage(ABILITY_CLAIM_FAIL, AbilityUtils.getUser(ctx.update()).getLanguageCode()), chatId); + send(ABILITY_CLAIM_FAIL, ctx); else { admins.add(id); - silent.send(getLocalizedMessage(ABILITY_CLAIM_SUCCESS, AbilityUtils.getUser(ctx.update()).getLanguageCode()), 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 send(String message, MessageContext ctx, String... args) { + return silent.send(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId()); + } + + private Optional sendMd(String message, MessageContext ctx, String... args) { + return silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId()); + } + + private Optional send(String message, Update upd, Long chatId) { + 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. *

@@ -599,7 +606,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Pair getContext(Trio 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()); } @@ -618,11 +625,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( - getLocalizedMessage( - CHECK_INPUT_FAIL, - AbilityUtils.getUser(trio.a()).getLanguageCode(), - abilityTokens, abilityTokens == 1 ? "input" : "inputs"), - getChatId(trio.a())); + getLocalizedMessage( + CHECK_INPUT_FAIL, + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityTokens, abilityTokens == 1 ? "input" : "inputs"), + getChatId(trio.a())); return isOk; } @@ -635,30 +642,30 @@ public abstract class AbilityBot extends TelegramLongPollingBot { if (!isOk) silent.send( - getLocalizedMessage( - CHECK_LOCALITY_FAIL, - AbilityUtils.getUser(trio.a()).getLanguageCode(), - abilityLocality.toString().toLowerCase()), - getChatId(trio.a())); + getLocalizedMessage( + CHECK_LOCALITY_FAIL, + AbilityUtils.getUser(trio.a()).getLanguageCode(), + abilityLocality.toString().toLowerCase()), + getChatId(trio.a())); return isOk; } boolean checkPrivacy(Trio 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( - getLocalizedMessage( - CHECK_PRIVACY_FAIL, - AbilityUtils.getUser(trio.a()).getLanguageCode()), - getChatId(trio.a())); + getLocalizedMessage( + CHECK_PRIVACY_FAIL, + AbilityUtils.getUser(trio.a()).getLanguageCode()), + getChatId(trio.a())); return isOk; } @@ -709,9 +716,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; @@ -729,15 +736,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()); } } @@ -765,6 +772,7 @@ public abstract class AbilityBot extends TelegramLongPollingBot { return sender.downloadFile(sender.execute(new GetFile().setFileId(fileId))); } + private String escape(String username) { return username.replace("_", "\\_"); } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java deleted file mode 100644 index 7cc08145..00000000 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/EndUser.java +++ /dev/null @@ -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}. - *

- * 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: - *

    - *
  1. First name
  2. - *
  3. Last name
  4. - *
  5. Username
  6. - *
- * 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(); - } -} diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/MessageContext.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/MessageContext.java index c3335af9..ca689cd1 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/MessageContext.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/MessageContext.java @@ -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; } diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java index 113d26b8..d88a99e3 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/objects/Reply.java @@ -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. *

- * 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 */ diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java index 81209885..a1aea560 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityUtils.java @@ -10,12 +10,14 @@ 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.*; /** @@ -204,4 +206,42 @@ public final class AbilityUtils { return getLocalizedMessage(messageCode, locale, arguments); } -} + /** + * The short name is one of the following: + *

    + *
  1. First name
  2. + *
  3. Last name
  4. + *
  5. Username
  6. + *
+ * 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(); + } +} \ No newline at end of file diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 75aa6df0..fdb8f92c 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -11,17 +11,15 @@ import org.telegram.telegrambots.api.objects.User; import java.io.IOException; -import static java.lang.Long.valueOf; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; import static org.telegram.abilitybots.api.bot.AbilityBotTest.mockContext; -import static org.telegram.abilitybots.api.bot.AbilityBotTest.newUser; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; public class AbilityBotI18nTest { - private static final User NO_LANGUAGE_USER = newUser(1, "first", "last", "username", null); - private static final User ITALIAN_USER = newUser(2, "first", "last", "username", "it-IT"); + 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; @@ -53,9 +51,9 @@ public class AbilityBotI18nTest { @Test public void missingPublicCommandsLocalizedCorrectly2() { - MessageContext context1 = mockContext(ITALIAN_USER); + MessageContext context = mockContext(ITALIAN_USER); - bot.reportCommands().action().accept(context1); + bot.reportCommands().action().accept(context); verify(silent, times(1)) .send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId()); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index 54e48308..c2fa81b7 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -38,8 +38,6 @@ import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; 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.EndUser.fromUser; 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; @@ -56,10 +54,8 @@ public class AbilityBotTest { 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 User TG_USER = newUser(1, "first", "last", "username", null); - public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername"); - public static final User TG_CREATOR = newUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); + 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; @@ -80,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 @@ -149,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(); @@ -163,33 +159,33 @@ public class AbilityBotTest { @Test public void canPromote() { - addUsers(MUSER); + addUsers(USER); MessageContext context = defaultContext(); bot.promoteAdmin().action().accept(context); Set actual = bot.admins(); - Set expected = newHashSet(MUSER.id()); + Set 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 actual = bot.blacklist(); - Set expected = newHashSet(MUSER.id()); + Set 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(); @@ -202,51 +198,49 @@ public class AbilityBotTest { @NotNull private MessageContext defaultContext() { - MessageContext context = mockContext(TG_CREATOR, GROUP_ID); - 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 = mockContext(TG_USER, GROUP_ID); - when(context.firstArg()).thenReturn(CREATOR.username()); + addUsers(USER, CREATOR); + MessageContext context = mockContext(USER, GROUP_ID, CREATOR.getUserName()); bot.banUser().action().accept(context); Set actual = bot.blacklist(); - Set expected = newHashSet(MUSER.id()); + Set 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 = mockContext(TG_CREATOR, GROUP_ID); + MessageContext context = mockContext(CREATOR, GROUP_ID); bot.claimCreator().action().accept(context); Set actual = bot.admins(); - Set expected = newHashSet(CREATOR.id()); + Set 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 = mockContext(TG_USER, GROUP_ID); + addUsers(USER); + MessageContext context = mockContext(USER, GROUP_ID); bot.claimCreator().action().accept(context); Set actual = bot.blacklist(); - Set expected = newHashSet(MUSER.id()); + Set expected = newHashSet(USER.getId()); assertEquals("Could not find user on the blacklist", expected, actual); actual = bot.admins(); @@ -256,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); @@ -273,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 expectedUserIds = ImmutableMap.of(MUSER.username(), MUSER.id()); - Map expectedUsers = ImmutableMap.of(MUSER.id(), MUSER); + Map expectedUserIds = ImmutableMap.of(USER.getUserName(), USER.getId()); + Map 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 expectedUserIds = ImmutableMap.of(changedUser.username(), changedUser.id()); - Map expectedUsers = ImmutableMap.of(changedUser.id(), changedUser); + Map expectedUserIds = ImmutableMap.of(changedUser.getUserName(), changedUser.getId()); + Map expectedUsers = ImmutableMap.of(changedUser.getId(), changedUser); assertEquals("User was not properly edited", bot.userIds(), expectedUserIds); assertEquals("User was not properly edited", expectedUsers, expectedUsers); } @@ -318,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() @@ -409,7 +403,7 @@ public class AbilityBotTest { Trio 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)); @@ -440,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 trio = Trio.of(update, ability, TEXT); when(message.getChatId()).thenReturn(GROUP_ID); - mockUser(update, message, user); + mockUser(update, message, USER); Pair actualPair = bot.getContext(trio); - Pair expectedPair = Pair.of(newContext(update, MUSER, GROUP_ID, TEXT), ability); + Pair expectedPair = Pair.of(newContext(update, USER, GROUP_ID, TEXT), ability); assertEquals("Unexpected result when fetching for context", expectedPair, actualPair); } @@ -549,7 +542,7 @@ public class AbilityBotTest { @Test public void canReportCommands() { - MessageContext context = mockContext(TG_USER, GROUP_ID); + MessageContext context = mockContext(USER, GROUP_ID); bot.reportCommands().action().accept(context); @@ -562,10 +555,9 @@ public class AbilityBotTest { } @NotNull - public static MessageContext mockContext(User user, long groupId) { + public static MessageContext mockContext(User user, long groupId, String... args) { Update update = mock(Update.class); Message message = mock(Message.class); - EndUser endUser = fromUser(user); when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); @@ -573,12 +565,7 @@ public class AbilityBotTest { when(message.getFrom()).thenReturn(user); when(message.hasText()).thenReturn(true); - MessageContext context = mock(MessageContext.class); - when(context.update()).thenReturn(update); - when(context.chatId()).thenReturn(groupId); - when(context.user()).thenReturn(endUser); - - return context; + return newContext(update, user, groupId, args); } @After @@ -587,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); @@ -615,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; } @@ -624,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); @@ -646,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); diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java index 7e53584b..502f70a1 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java @@ -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 users = db.getMap(USERS); + Map users = db.getMap(USERS); Map 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 originalUsers = newHashMap(users); + Map originalUsers = newHashMap(users); String beforeBackupInfo = db.info(USERS); Object jsonBackup = db.backup(); db.clear(); boolean recovered = db.recover(jsonBackup); - Map recoveredUsers = db.getMap(USERS); + Map 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 users = db.getSet(USERS); + public void canFallbackDBIfRecoveryFails() { + Set users = db.getSet(USERS); users.add(CREATOR); - users.add(MUSER); + users.add(USER); - Set originalSet = newHashSet(users); + Set originalSet = newHashSet(users); Object jsonBackup = db.backup(); String corruptBackup = "!@#$" + String.valueOf(jsonBackup); boolean recovered = db.recover(corruptBackup); - Set recoveredSet = db.getSet(USERS); + Set 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); } From 7d216a1faed78ae2447bc7f6dc257bfa59c54437 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Tue, 22 May 2018 03:12:47 -0400 Subject: [PATCH 33/41] Fix todos, all ability bot messages are now properly localized --- .../abilitybots/api/bot/AbilityBot.java | 31 +++++++++++-------- .../api/bot/AbilityBotI18nTest.java | 19 ++++++------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 4b0c03fd..d4d2c2ea 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -260,13 +260,14 @@ 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 * @return the id of the user */ - protected int getUserIdSendError(String username, long chatId) { + protected int getUserIdSendError(String username, MessageContext ctx) { try { return getUser(username).getId(); } catch (IllegalStateException ex) { - silent.send(getLocalizedMessage(USER_NOT_FOUND, "", username), chatId); // TODO how to retrieve language? + silent.send(getLocalizedMessage(USER_NOT_FOUND, ctx.user().getLanguageCode(), username), ctx.chatId()); throw propagate(ex); } } @@ -361,21 +362,24 @@ public abstract class AbilityBot extends TelegramLongPollingBot { .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)) { - send(ABILITY_RECOVER_SUCCESS, update, chatId); + send(ABILITY_RECOVER_SUCCESS, update); } else { - send(ABILITY_RECOVER_FAIL, update, chatId); + send(ABILITY_RECOVER_FAIL, update); } } catch (Exception e) { BotLogger.error("Could not recover DB from backup", TAG, e); - send(ABILITY_RECOVER_ERROR, update, chatId); + send(ABILITY_RECOVER_ERROR, update); } - }, MESSAGE, DOCUMENT, REPLY, isReplyTo(getLocalizedMessage(ABILITY_RECOVER_SUCCESS, ""))) // TODO how to retrieve language? + }, MESSAGE, DOCUMENT, REPLY) .build(); } @@ -396,7 +400,7 @@ 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 @@ -432,7 +436,7 @@ 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 blacklist = blacklist(); @@ -457,7 +461,7 @@ 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 admins = admins(); if (admins.contains(userId)) @@ -481,7 +485,7 @@ 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 admins = admins(); if (admins.remove(userId)) { @@ -533,7 +537,8 @@ public abstract class AbilityBot extends TelegramLongPollingBot { return silent.sendMd(getLocalizedMessage(message, ctx.user().getLanguageCode(), args), ctx.chatId()); } - private Optional send(String message, Update upd, Long chatId) { + private Optional send(String message, Update upd) { + Long chatId = upd.getMessage().getChatId(); return silent.send(getLocalizedMessage(message, AbilityUtils.getUser(upd).getLanguageCode()), chatId); } diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index fdb8f92c..808b4eda 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -12,7 +12,8 @@ import org.telegram.telegrambots.api.objects.User; import java.io.IOException; import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.mockito.Mockito.*; +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; @@ -37,6 +38,7 @@ public class AbilityBotI18nTest { bot.sender = sender; bot.silent = silent; + } @Test @@ -59,7 +61,6 @@ public class AbilityBotI18nTest { .send("Non sono presenti comandi pubblici.", ITALIAN_USER.getId()); } - @After public void tearDown() throws IOException { db.clear(); @@ -68,13 +69,13 @@ public class AbilityBotI18nTest { public static class NoPublicCommandsBot extends AbilityBot { - protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { - super(botToken, botUsername, db); - } + protected NoPublicCommandsBot(String botToken, String botUsername, DBContext db) { + super(botToken, botUsername, db); + } - @Override - public int creatorId() { - return 0; - } + @Override + public int creatorId() { + return 1; + } } } From f78fd046e9cc3abd6ac2a491607f5456cb01e657 Mon Sep 17 00:00:00 2001 From: Chase Date: Wed, 23 May 2018 09:17:45 +0200 Subject: [PATCH 34/41] Added Documentation for deprecated methods --- .../telegram/telegrambots/bots/AbsSender.java | 289 +++++++++++++++++- 1 file changed, 288 insertions(+), 1 deletion(-) diff --git a/telegrambots-meta/src/main/java/org/telegram/telegrambots/bots/AbsSender.java b/telegrambots-meta/src/main/java/org/telegram/telegrambots/bots/AbsSender.java index b0734bf8..57411c79 100644 --- a/telegrambots-meta/src/main/java/org/telegram/telegrambots/bots/AbsSender.java +++ b/telegrambots-meta/src/main/java/org/telegram/telegrambots/bots/AbsSender.java @@ -54,7 +54,10 @@ public abstract class AbsSender { } // Send Requests - + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendMessage + */ @Deprecated public final Message sendMessage(SendMessage sendMessage) throws TelegramApiException { if (sendMessage == null) { @@ -64,6 +67,10 @@ public abstract class AbsSender { return sendApiMethod(sendMessage); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see AnswerInlineQuery + */ @Deprecated public final Boolean answerInlineQuery(AnswerInlineQuery answerInlineQuery) throws TelegramApiException { if (answerInlineQuery == null) { @@ -73,6 +80,10 @@ public abstract class AbsSender { return sendApiMethod(answerInlineQuery); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendChatAction + */ @Deprecated public final Boolean sendChatAction(SendChatAction sendChatAction) throws TelegramApiException { if (sendChatAction == null) { @@ -82,6 +93,10 @@ public abstract class AbsSender { return sendApiMethod(sendChatAction); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see ForwardMessage + */ @Deprecated public final Message forwardMessage(ForwardMessage forwardMessage) throws TelegramApiException { if (forwardMessage == null) { @@ -91,6 +106,10 @@ public abstract class AbsSender { return sendApiMethod(forwardMessage); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendLocation + */ @Deprecated public final Message sendLocation(SendLocation sendLocation) throws TelegramApiException { if (sendLocation == null) { @@ -100,6 +119,10 @@ public abstract class AbsSender { return sendApiMethod(sendLocation); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendVenue + */ @Deprecated public final Message sendVenue(SendVenue sendVenue) throws TelegramApiException { if (sendVenue == null) { @@ -109,6 +132,10 @@ public abstract class AbsSender { return sendApiMethod(sendVenue); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendContact + */ @Deprecated public final Message sendContact(SendContact sendContact) throws TelegramApiException { if (sendContact == null) { @@ -118,6 +145,10 @@ public abstract class AbsSender { return sendApiMethod(sendContact); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see KickChatMember + */ @Deprecated public final Boolean kickMember(KickChatMember kickChatMember) throws TelegramApiException { if (kickChatMember == null) { @@ -126,6 +157,10 @@ public abstract class AbsSender { return sendApiMethod(kickChatMember); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see UnbanChatMember + */ @Deprecated public final Boolean unbanMember(UnbanChatMember unbanChatMember) throws TelegramApiException { if (unbanChatMember == null) { @@ -134,6 +169,10 @@ public abstract class AbsSender { return sendApiMethod(unbanChatMember); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see LeaveChat + */ @Deprecated public final Boolean leaveChat(LeaveChat leaveChat) throws TelegramApiException { if (leaveChat == null) { @@ -142,6 +181,10 @@ public abstract class AbsSender { return sendApiMethod(leaveChat); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetChat + */ @Deprecated public final Chat getChat(GetChat getChat) throws TelegramApiException { if (getChat == null) { @@ -150,6 +193,10 @@ public abstract class AbsSender { return sendApiMethod(getChat); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see ExportChatInviteLink + */ @Deprecated public final String exportChatInviteLink(ExportChatInviteLink exportChatInviteLink) throws TelegramApiException { if (exportChatInviteLink == null) { @@ -158,6 +205,10 @@ public abstract class AbsSender { return sendApiMethod(exportChatInviteLink); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetChatAdministrators + */ @Deprecated public final List getChatAdministrators(GetChatAdministrators getChatAdministrators) throws TelegramApiException { if (getChatAdministrators == null) { @@ -166,6 +217,10 @@ public abstract class AbsSender { return sendApiMethod(getChatAdministrators); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetChatMember + */ @Deprecated public final ChatMember getChatMember(GetChatMember getChatMember) throws TelegramApiException { if (getChatMember == null) { @@ -174,6 +229,10 @@ public abstract class AbsSender { return sendApiMethod(getChatMember); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetChatMemberCount + */ @Deprecated public final Integer getChatMemberCount(GetChatMemberCount getChatMemberCount) throws TelegramApiException { if (getChatMemberCount == null) { @@ -182,6 +241,10 @@ public abstract class AbsSender { return sendApiMethod(getChatMemberCount); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see EditMessageText + */ @Deprecated public final Serializable editMessageText(EditMessageText editMessageText) throws TelegramApiException { if (editMessageText == null) { @@ -190,6 +253,10 @@ public abstract class AbsSender { return sendApiMethod(editMessageText); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see EditMessageCaption + */ @Deprecated public final Serializable editMessageCaption(EditMessageCaption editMessageCaption) throws TelegramApiException { if (editMessageCaption == null) { @@ -198,6 +265,10 @@ public abstract class AbsSender { return sendApiMethod(editMessageCaption); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see EditMessageReplyMarkup + */ @Deprecated public final Serializable editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup) throws TelegramApiException { if (editMessageReplyMarkup == null) { @@ -206,6 +277,10 @@ public abstract class AbsSender { return sendApiMethod(editMessageReplyMarkup); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see AnswerCallbackQuery + */ @Deprecated public final Boolean answerCallbackQuery(AnswerCallbackQuery answerCallbackQuery) throws TelegramApiException { if (answerCallbackQuery == null) { @@ -214,6 +289,10 @@ public abstract class AbsSender { return sendApiMethod(answerCallbackQuery); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetUserProfilePhotos + */ @Deprecated public final UserProfilePhotos getUserProfilePhotos(GetUserProfilePhotos getUserProfilePhotos) throws TelegramApiException { if (getUserProfilePhotos == null) { @@ -223,6 +302,10 @@ public abstract class AbsSender { return sendApiMethod(getUserProfilePhotos); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetFile + */ @Deprecated public final File getFile(GetFile getFile) throws TelegramApiException { if(getFile == null){ @@ -243,6 +326,10 @@ public abstract class AbsSender { return sendApiMethod(getWebhookInfo); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SetGameScore + */ @Deprecated public final Serializable setGameScore(SetGameScore setGameScore) throws TelegramApiException { if(setGameScore == null){ @@ -251,6 +338,10 @@ public abstract class AbsSender { return sendApiMethod(setGameScore); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see GetGameHighScores + */ @Deprecated public final Serializable getGameHighScores(GetGameHighScores getGameHighScores) throws TelegramApiException { if(getGameHighScores == null){ @@ -259,6 +350,10 @@ public abstract class AbsSender { return sendApiMethod(getGameHighScores); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendGame + */ @Deprecated public final Message sendGame(SendGame sendGame) throws TelegramApiException { if(sendGame == null){ @@ -267,6 +362,10 @@ public abstract class AbsSender { return sendApiMethod(sendGame); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see DeleteWebhook + */ @Deprecated public final Boolean deleteWebhook(DeleteWebhook deleteWebhook) throws TelegramApiException { if(deleteWebhook == null){ @@ -275,6 +374,10 @@ public abstract class AbsSender { return sendApiMethod(deleteWebhook); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SendInvoice + */ @Deprecated public final Message sendInvoice(SendInvoice sendInvoice) throws TelegramApiException { if(sendInvoice == null){ @@ -283,6 +386,10 @@ public abstract class AbsSender { return sendApiMethod(sendInvoice); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see AnswerShippingQuery + */ @Deprecated public final Boolean answerShippingQuery(AnswerShippingQuery answerShippingQuery) throws TelegramApiException { if(answerShippingQuery == null){ @@ -291,6 +398,10 @@ public abstract class AbsSender { return sendApiMethod(answerShippingQuery); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see AnswerPreCheckoutQuery + */ @Deprecated public final Boolean answerPreCheckoutQuery(AnswerPreCheckoutQuery answerPreCheckoutQuery) throws TelegramApiException { if(answerPreCheckoutQuery == null){ @@ -299,6 +410,10 @@ public abstract class AbsSender { return sendApiMethod(answerPreCheckoutQuery); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see DeleteMessage + */ @Deprecated public final Boolean deleteMessage(DeleteMessage deleteMessage) throws TelegramApiException { if(deleteMessage == null){ @@ -307,6 +422,10 @@ public abstract class AbsSender { return sendApiMethod(deleteMessage); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see DeleteChatPhoto + */ @Deprecated public final Boolean deleteChatPhoto(DeleteChatPhoto deleteChatPhoto) throws TelegramApiException { if(deleteChatPhoto == null){ @@ -315,6 +434,10 @@ public abstract class AbsSender { return sendApiMethod(deleteChatPhoto); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see PinChatMessage + */ @Deprecated public final Boolean pinChatMessage(PinChatMessage pinChatMessage) throws TelegramApiException { if(pinChatMessage == null){ @@ -323,6 +446,10 @@ public abstract class AbsSender { return sendApiMethod(pinChatMessage); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see UnpinChatMessage + */ @Deprecated public final Boolean unpinChatMessage(UnpinChatMessage unpinChatMessage) throws TelegramApiException { if(unpinChatMessage == null){ @@ -331,6 +458,10 @@ public abstract class AbsSender { return sendApiMethod(unpinChatMessage); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see PromoteChatMember + */ @Deprecated public final Boolean promoteChatMember(PromoteChatMember promoteChatMember) throws TelegramApiException { if(promoteChatMember == null){ @@ -339,6 +470,10 @@ public abstract class AbsSender { return sendApiMethod(promoteChatMember); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see RestrictChatMember + */ @Deprecated public final Boolean restrictChatMember(RestrictChatMember restrictChatMember) throws TelegramApiException { if(restrictChatMember == null){ @@ -347,6 +482,10 @@ public abstract class AbsSender { return sendApiMethod(restrictChatMember); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SetChatDescription + */ @Deprecated public final Boolean setChatDescription(SetChatDescription setChatDescription) throws TelegramApiException { if(setChatDescription == null){ @@ -355,6 +494,10 @@ public abstract class AbsSender { return sendApiMethod(setChatDescription); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) execute} Method instead + * @see SetChatTitle + */ @Deprecated public final Boolean setChatTitle(SetChatTitle setChatTitle) throws TelegramApiException { if(setChatTitle == null){ @@ -365,6 +508,10 @@ public abstract class AbsSender { // Send Requests Async + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendMessage + */ @Deprecated public final void sendMessageAsync(SendMessage sendMessage, SentCallback sentCallback) throws TelegramApiException { if (sendMessage == null) { @@ -378,6 +525,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendMessage, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see AnswerInlineQuery + */ @Deprecated public final void answerInlineQueryAsync(AnswerInlineQuery answerInlineQuery, SentCallback sentCallback) throws TelegramApiException { if (answerInlineQuery == null) { @@ -391,6 +542,10 @@ public abstract class AbsSender { sendApiMethodAsync(answerInlineQuery, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendChatAction + */ @Deprecated public final void sendChatActionAsync(SendChatAction sendChatAction, SentCallback sentCallback) throws TelegramApiException { if (sendChatAction == null) { @@ -404,6 +559,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendChatAction, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see ForwardMessage + */ @Deprecated public final void forwardMessageAsync(ForwardMessage forwardMessage, SentCallback sentCallback) throws TelegramApiException { if (forwardMessage == null) { @@ -417,6 +576,10 @@ public abstract class AbsSender { sendApiMethodAsync(forwardMessage, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendLocation + */ @Deprecated public final void sendLocationAsync(SendLocation sendLocation, SentCallback sentCallback) throws TelegramApiException { if (sendLocation == null) { @@ -430,6 +593,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendLocation, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendVenue + */ @Deprecated public final void sendVenueAsync(SendVenue sendVenue, SentCallback sentCallback) throws TelegramApiException { if (sendVenue == null) { @@ -443,6 +610,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendVenue, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendContact + */ @Deprecated public final void sendContactAsync(SendContact sendContact, SentCallback sentCallback) throws TelegramApiException { if (sendContact == null) { @@ -455,6 +626,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendContact, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see KickChatMember + */ @Deprecated public final void kickMemberAsync(KickChatMember kickChatMember, SentCallback sentCallback) throws TelegramApiException { if (kickChatMember == null) { @@ -467,6 +642,10 @@ public abstract class AbsSender { sendApiMethodAsync(kickChatMember, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see UnbanChatMember + */ @Deprecated public final void unbanMemberAsync(UnbanChatMember unbanChatMember, SentCallback sentCallback) throws TelegramApiException { if (unbanChatMember == null) { @@ -479,6 +658,10 @@ public abstract class AbsSender { sendApiMethodAsync(unbanChatMember, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see LeaveChat + */ @Deprecated public final void leaveChatAsync(LeaveChat leaveChat, SentCallback sentCallback) throws TelegramApiException { if (leaveChat == null) { @@ -490,6 +673,10 @@ public abstract class AbsSender { sendApiMethodAsync(leaveChat, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetChat + */ @Deprecated public final void getChatAsync(GetChat getChat, SentCallback sentCallback) throws TelegramApiException { if (getChat == null) { @@ -501,6 +688,10 @@ public abstract class AbsSender { sendApiMethodAsync(getChat, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see ExportChatInviteLink + */ @Deprecated public final void exportChatInviteLinkAsync(ExportChatInviteLink exportChatInviteLink, SentCallback sentCallback) throws TelegramApiException { if (exportChatInviteLink == null) { @@ -512,6 +703,10 @@ public abstract class AbsSender { sendApiMethodAsync(exportChatInviteLink, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetChatAdministrators + */ @Deprecated public final void getChatAdministratorsAsync(GetChatAdministrators getChatAdministrators, SentCallback> sentCallback) throws TelegramApiException { if (getChatAdministrators == null) { @@ -523,6 +718,10 @@ public abstract class AbsSender { sendApiMethodAsync(getChatAdministrators, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetChatMember + */ @Deprecated public final void getChatMemberAsync(GetChatMember getChatMember, SentCallback sentCallback) throws TelegramApiException { if (getChatMember == null) { @@ -534,6 +733,10 @@ public abstract class AbsSender { sendApiMethodAsync(getChatMember, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetChatMemberCount + */ @Deprecated public final void getChatMemberCountAsync(GetChatMemberCount getChatMemberCount, SentCallback sentCallback) throws TelegramApiException { if (getChatMemberCount == null) { @@ -546,6 +749,10 @@ public abstract class AbsSender { sendApiMethodAsync(getChatMemberCount, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see EditMessageText + */ @Deprecated public final void editMessageTextAsync(EditMessageText editMessageText, SentCallback sentCallback) throws TelegramApiException { if (editMessageText == null) { @@ -558,6 +765,10 @@ public abstract class AbsSender { sendApiMethodAsync(editMessageText, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see EditMessageCaption + */ @Deprecated public final void editMessageCaptionAsync(EditMessageCaption editMessageCaption, SentCallback sentCallback) throws TelegramApiException { if (editMessageCaption == null) { @@ -570,6 +781,10 @@ public abstract class AbsSender { sendApiMethodAsync(editMessageCaption, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see EditMessageReplyMarkup + */ @Deprecated public final void editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup, SentCallback sentCallback) throws TelegramApiException { if (editMessageReplyMarkup == null) { @@ -582,6 +797,10 @@ public abstract class AbsSender { sendApiMethodAsync(editMessageReplyMarkup, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see AnswerCallbackQuery + */ @Deprecated public final void answerCallbackQueryAsync(AnswerCallbackQuery answerCallbackQuery, SentCallback sentCallback) throws TelegramApiException { if (answerCallbackQuery == null) { @@ -594,6 +813,10 @@ public abstract class AbsSender { sendApiMethodAsync(answerCallbackQuery, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetUserProfilePhotos + */ @Deprecated public final void getUserProfilePhotosAsync(GetUserProfilePhotos getUserProfilePhotos, SentCallback sentCallback) throws TelegramApiException { if (getUserProfilePhotos == null) { @@ -606,6 +829,10 @@ public abstract class AbsSender { sendApiMethodAsync(getUserProfilePhotos, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetFile + */ @Deprecated public final void getFileAsync(GetFile getFile, SentCallback sentCallback) throws TelegramApiException { if (getFile == null) { @@ -632,6 +859,10 @@ public abstract class AbsSender { sendApiMethodAsync(new GetWebhookInfo(), sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SetGameScore + */ @Deprecated public final void setGameScoreAsync(SetGameScore setGameScore, SentCallback sentCallback) throws TelegramApiException { if (setGameScore == null) { @@ -643,6 +874,10 @@ public abstract class AbsSender { sendApiMethodAsync(setGameScore, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see GetGameHighScores + */ @Deprecated public final void getGameHighScoresAsync(GetGameHighScores getGameHighScores, SentCallback> sentCallback) throws TelegramApiException { if (getGameHighScores == null) { @@ -654,6 +889,10 @@ public abstract class AbsSender { sendApiMethodAsync(getGameHighScores, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendGame + */ @Deprecated public final void sendGameAsync(SendGame sendGame, SentCallback sentCallback) throws TelegramApiException { if (sendGame == null) { @@ -665,6 +904,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendGame, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see DeleteWebhook + */ @Deprecated public final void deleteWebhook(DeleteWebhook deleteWebhook, SentCallback sentCallback) throws TelegramApiException { if (deleteWebhook == null) { @@ -676,6 +919,10 @@ public abstract class AbsSender { sendApiMethodAsync(deleteWebhook, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SendInvoice + */ @Deprecated public final void sendInvoice(SendInvoice sendInvoice, SentCallback sentCallback) throws TelegramApiException { if (sendInvoice == null) { @@ -687,6 +934,10 @@ public abstract class AbsSender { sendApiMethodAsync(sendInvoice, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see AnswerShippingQuery + */ @Deprecated public final void answerShippingQuery(AnswerShippingQuery answerShippingQuery, SentCallback sentCallback) throws TelegramApiException { if (answerShippingQuery == null) { @@ -698,6 +949,10 @@ public abstract class AbsSender { sendApiMethodAsync(answerShippingQuery, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see AnswerPreCheckoutQuery + */ @Deprecated public final void answerPreCheckoutQuery(AnswerPreCheckoutQuery answerPreCheckoutQuery, SentCallback sentCallback) throws TelegramApiException { if (answerPreCheckoutQuery == null) { @@ -709,6 +964,10 @@ public abstract class AbsSender { sendApiMethodAsync(answerPreCheckoutQuery, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see DeleteMessage + */ @Deprecated public final void deleteMessage(DeleteMessage deleteMessage, SentCallback sentCallback) throws TelegramApiException { if (deleteMessage == null) { @@ -720,6 +979,10 @@ public abstract class AbsSender { sendApiMethodAsync(deleteMessage, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see DeleteChatPhoto + */ @Deprecated public final void deleteChatPhoto(DeleteChatPhoto deleteChatPhoto, SentCallback sentCallback) throws TelegramApiException { if (deleteChatPhoto == null) { @@ -731,6 +994,10 @@ public abstract class AbsSender { sendApiMethodAsync(deleteChatPhoto, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see PinChatMessage + */ @Deprecated public final void pinChatMessage(PinChatMessage pinChatMessage, SentCallback sentCallback) throws TelegramApiException { if (pinChatMessage == null) { @@ -742,6 +1009,10 @@ public abstract class AbsSender { sendApiMethodAsync(pinChatMessage, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see UnpinChatMessage + */ @Deprecated public final void unpinChatMessage(UnpinChatMessage unpinChatMessage, SentCallback sentCallback) throws TelegramApiException { if (unpinChatMessage == null) { @@ -753,6 +1024,10 @@ public abstract class AbsSender { sendApiMethodAsync(unpinChatMessage, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see PromoteChatMember + */ @Deprecated public final void promoteChatMember(PromoteChatMember promoteChatMember, SentCallback sentCallback) throws TelegramApiException { if (promoteChatMember == null) { @@ -764,6 +1039,10 @@ public abstract class AbsSender { sendApiMethodAsync(promoteChatMember, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see RestrictChatMember + */ @Deprecated public final void restrictChatMember(RestrictChatMember restrictChatMember, SentCallback sentCallback) throws TelegramApiException { if (restrictChatMember == null) { @@ -775,6 +1054,10 @@ public abstract class AbsSender { sendApiMethodAsync(restrictChatMember, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SetChatDescription + */ @Deprecated public final void setChatDescription(SetChatDescription setChatDescription, SentCallback sentCallback) throws TelegramApiException { if (setChatDescription == null) { @@ -786,6 +1069,10 @@ public abstract class AbsSender { sendApiMethodAsync(setChatDescription, sentCallback); } + /** + * Deprecated. Use {@link #execute(BotApiMethod) executeAsync} Method instead + * @see SetChatTitle + */ @Deprecated public final void setChatTitle(SetChatTitle setChatTitle, SentCallback sentCallback) throws TelegramApiException { if (setChatTitle == null) { From 889fd4683489135a1be3e427f0c2ce01156a230a Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Wed, 23 May 2018 18:59:03 -0400 Subject: [PATCH 35/41] Expose abilities and replies, add report command and reformat /commands, closes #436 --- .../abilitybots/api/bot/AbilityBot.java | 134 +++++++++++++++--- .../src/main/resources/messages.properties | 2 +- .../api/bot/AbilityBotI18nTest.java | 8 +- .../abilitybots/api/bot/AbilityBotTest.java | 105 +++++++------- .../test/resources/messages_it_IT.properties | 2 +- 5 files changed, 171 insertions(+), 80 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index d4d2c2ea..95d39f5d 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -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 abilities() { + return abilities; + } + + /** + * @return the immutable list carrying the embedded replies + */ + public List replies() { + return replies; + } + /** * This method contains the stream of actions that are applied on any update. *

@@ -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: + *

+ * PUBLIC + *

+ * [command1] - [description1] + *

+ * [command2] - [description2] + *

+ * GROUP_ADMIN + *

+ * [command1] - [description1] + *

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

@@ -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 admins = admins(); - int id = creatorId(); + Set 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::builder, + (b, a) -> b.put(a.name(), a), + (b1, b2) -> b1.putAll(b2.build())) + .build(); Stream methodReplies = stream(this.getClass().getMethods()) .filter(method -> method.getReturnType().equals(Reply.class)) @@ -561,7 +639,11 @@ public abstract class AbilityBot extends TelegramLongPollingBot { Stream abilityReplies = abilities.values().stream() .flatMap(ability -> ability.replies().stream()); - replies = Stream.concat(methodReplies, abilityReplies).collect(toList()); + replies = Stream.concat(methodReplies, abilityReplies).collect( + ImmutableList::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)); diff --git a/telegrambots-abilities/src/main/resources/messages.properties b/telegrambots-abilities/src/main/resources/messages.properties index a8495e84..57e2485b 100644 --- a/telegrambots-abilities/src/main/resources/messages.properties +++ b/telegrambots-abilities/src/main/resources/messages.properties @@ -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. diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java index 808b4eda..fd1b75f9 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotI18nTest.java @@ -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 diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java index c2fa81b7..d7cdb9d7 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/AbilityBotTest.java @@ -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 actual = bot.blacklist(); - Set 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 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 trioOneArg = Trio.of(update, abilityWithOneInput, TEXT); Trio 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 docTrio = Trio.of(update, documentAbility, TEXT); Trio 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; - } } diff --git a/telegrambots-abilities/src/test/resources/messages_it_IT.properties b/telegrambots-abilities/src/test/resources/messages_it_IT.properties index c5656cc1..52f1377d 100644 --- a/telegrambots-abilities/src/test/resources/messages_it_IT.properties +++ b/telegrambots-abilities/src/test/resources/messages_it_IT.properties @@ -1 +1 @@ -ability.commands.notFound=Non sono presenti comandi pubblici. \ No newline at end of file +ability.commands.notFound=Non sono presenti comandi disponibile. \ No newline at end of file From 7bee69c68d5347f29ca6c0e569924946a53408a2 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Wed, 23 May 2018 19:11:26 -0400 Subject: [PATCH 36/41] Fix wiki doc and javadoc --- TelegramBots.wiki/abilities/Simple-Example.md | 5 +++-- .../java/org/telegram/abilitybots/api/bot/AbilityBot.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/TelegramBots.wiki/abilities/Simple-Example.md b/TelegramBots.wiki/abilities/Simple-Example.md index 3e2419e3..9b99679b 100644 --- a/TelegramBots.wiki/abilities/Simple-Example.md +++ b/TelegramBots.wiki/abilities/Simple-Example.md @@ -102,8 +102,9 @@ If you're in doubt that you're missing some code, the full code example can be i Go ahead and "/hello" to your bot. It should respond back with "Hello World!". Since you've implemented an AbilityBot, you get **factory abilities** as well. Try: -* /commands - Prints all commands supported by the bot - * This will essentially print "hello - says hello world!". Yes! This is the information we supplied to the ability. The bot prints the commands in the format accepted by BotFather. So, whenever you change, add or remove commands, you can simply /commands and forward that message to BotFather. +* /report - Prints all user-defined commands supported by the bot + * This will essentially print "hello - says hello world!". Yes! This is the information we supplied to the ability. The bot prints the commands in the format accepted by BotFather. So, whenever you change, add or remove commands, you can simply /report and forward that message to BotFather. +* /commands - Prints all commands exposed by the bot (factory and user-defined, with and without info) * /claim - Claims this bot * /backup - returns a backup of the bot database * /recover - recovers the database diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java index 95d39f5d..a86841cb 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/AbilityBot.java @@ -72,10 +72,11 @@ import static org.telegram.abilitybots.api.util.AbilityUtils.*; *

  • Sets the user as the {@link Privacy#CREATOR} of the bot
  • *
  • Only the user with the ID returned by {@link AbilityBot#creatorId()} can genuinely claim the bot
  • * - *
  • /commands - reports all user-defined commands (abilities)
  • + *
  • /report - reports all user-defined commands (abilities)
  • *
      *
    • The same format acceptable by BotFather
    • *
    + *
  • /commands - returns a list of all possible bot commands based on the privacy of the requesting user
  • *
  • /backup - returns a backup of the bot database
  • *
  • /recover - recovers the database
  • *
  • /promote @username - promotes user to bot admin
  • From 714857555dafe7b5f3ca3e588ad107d54b080ea7 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Sun, 27 May 2018 04:16:19 -0400 Subject: [PATCH 37/41] Enhance DBContext with a single variable getter, pushes forward #425 --- .../abilitybots/api/db/DBContext.java | 7 +++ .../abilitybots/api/db/MapDBContext.java | 6 +++ .../telegram/abilitybots/api/db/MapDBVar.java | 49 +++++++++++++++++++ .../org/telegram/abilitybots/api/db/Var.java | 19 +++++++ .../abilitybots/api/db/MapDBContextTest.java | 17 +++++++ 5 files changed, 98 insertions(+) create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBVar.java create mode 100644 telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/Var.java diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java index eda5e9c1..cd25683b 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java @@ -39,6 +39,13 @@ public interface DBContext extends Closeable { */ Set getSet(String name); + /** + * @param name the unique name of the {@link Set} + * @param the type that the Set holds + * @return the Set with the specified name + */ + Var getVar(String name); + /** * @return a high-level summary of the database structures (Sets, Lists, Maps, ...) present. */ diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java index e9d2efd6..c7e5b018 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBContext.java @@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.db; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import org.mapdb.Atomic; import org.mapdb.DB; import org.mapdb.DBMaker; import org.mapdb.Serializer; @@ -93,6 +94,11 @@ public class MapDBContext implements DBContext { return (Set) db.hashSet(name, JAVA).createOrOpen(); } + @Override + public Var getVar(String name) { + return new MapDBVar<>((Atomic.Var) db.atomicVar(name).createOrOpen()); + } + @Override public String summary() { return stream(db.getAllNames().spliterator(), false) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBVar.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBVar.java new file mode 100644 index 00000000..a325fd36 --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/MapDBVar.java @@ -0,0 +1,49 @@ +package org.telegram.abilitybots.api.db; + +import com.google.common.base.MoreObjects; +import org.mapdb.Atomic; + +import java.util.Objects; + +/** + * The MapDB variant for {@link DBContext#getVar(String)}. + * + * @param the type of the inner variable + */ +public final class MapDBVar implements Var { + private Atomic.Var var; + + public MapDBVar(Atomic.Var var) { + this.var = var; + } + + @Override + public T get() { + return var.get(); + } + + @Override + public void set(T var) { + this.var.set(var); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MapDBVar mapDBVar = (MapDBVar) o; + return Objects.equals(var, mapDBVar.var); + } + + @Override + public int hashCode() { + return Objects.hash(var); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("var", var) + .toString(); + } +} diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/Var.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/Var.java new file mode 100644 index 00000000..b6ec4593 --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/Var.java @@ -0,0 +1,19 @@ +package org.telegram.abilitybots.api.db; + +/** + * The interface governing a variable for abstract getters and setters. + * @param the type of the variable + * + * @author Abbas Abou Daya + */ +public interface Var { + /** + * @return the variable contained + */ + T get(); + + /** + * @param var the new variable value + */ + void set(T var); +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java index 502f70a1..0f85ce36 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/db/MapDBContextTest.java @@ -100,6 +100,23 @@ public class MapDBContextTest { db.info(TEST); } + @Test + public void canGetAndSetVariables() { + String varName = "somevar"; + Var var = db.getVar(varName); + var.set(CREATOR); + db.commit(); + + var = db.getVar(varName); + assertEquals(var.get(), CREATOR); + + var.set(USER); + db.commit(); + + Var changedVar = db.getVar(varName); + assertEquals(changedVar.get(), USER); + } + @After public void tearDown() throws IOException { db.clear(); From afebcaeb76c1ad8b9c6b4044d2c3e360d1201fab Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Sun, 27 May 2018 04:20:04 -0400 Subject: [PATCH 38/41] Fix javadoc --- .../java/org/telegram/abilitybots/api/db/DBContext.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java index cd25683b..c1963ce1 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java @@ -40,9 +40,9 @@ public interface DBContext extends Closeable { Set getSet(String name); /** - * @param name the unique name of the {@link Set} - * @param the type that the Set holds - * @return the Set with the specified name + * @param name the unique name of the {@link Var} + * @param the type that the variable holds + * @return the variable with the specified name */ Var getVar(String name); From 493568649acb89d31f272c41271a51dbb75c2aeb Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Sat, 9 Jun 2018 15:02:22 -0300 Subject: [PATCH 39/41] Spring Boot autoconfiguration improvements. Unit test refactoring, now with autoconfiguration integration testing. Bug fix that did not initialize the bots, even with the deprecated @EnableTelegramBots * [Removed - If users wish to disable, just inform telegrambots.enabled = false]. Upgrade Spring boot 2.0.2 --- telegrambots-spring-boot-starter/README.md | 4 +- telegrambots-spring-boot-starter/pom.xml | 19 ++- .../starter/EnableTelegramBots.java | 10 -- .../starter/TelegramBotInitializer.java | 45 ++++++ .../TelegramBotStarterConfiguration.java | 77 +++------- .../main/resources/META-INF/spring.factories | 1 + .../TestTelegramBotStarterConfiguration.java | 137 +++++++++++------- 7 files changed, 172 insertions(+), 121 deletions(-) delete mode 100644 telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/EnableTelegramBots.java create mode 100644 telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotInitializer.java create mode 100644 telegrambots-spring-boot-starter/src/main/resources/META-INF/spring.factories diff --git a/telegrambots-spring-boot-starter/README.md b/telegrambots-spring-boot-starter/README.md index 8bb3f2a8..34b2cff5 100644 --- a/telegrambots-spring-boot-starter/README.md +++ b/telegrambots-spring-boot-starter/README.md @@ -25,7 +25,7 @@ Usage **Gradle** ```gradle - compile "org.telegram:telegrambots-spring-boot-starter:3.6" + compile "org.telegram:telegrambots-spring-boot-starter:3.6.1" ``` Motivation @@ -39,8 +39,6 @@ Your main spring boot class should look like this: ```java @SpringBootApplication -//Add this annotation to enable automatic bots initializing -@EnableTelegramBots public class YourApplicationMainClass { public static void main(String[] args) { diff --git a/telegrambots-spring-boot-starter/pom.xml b/telegrambots-spring-boot-starter/pom.xml index 72ed3705..dba8f1e1 100644 --- a/telegrambots-spring-boot-starter/pom.xml +++ b/telegrambots-spring-boot-starter/pom.xml @@ -60,7 +60,7 @@ UTF-8 UTF-8 3.6.1 - 1.5.10.RELEASE + 2.0.2.RELEASE @@ -75,24 +75,41 @@ spring-boot ${spring-boot.version} + org.springframework.boot spring-boot-autoconfigure ${spring-boot.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + test + + + org.assertj + assertj-core + test + 3.9.1 + + org.mockito mockito-all 2.0.2-beta test + junit junit 4.11 test + diff --git a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/EnableTelegramBots.java b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/EnableTelegramBots.java deleted file mode 100644 index 68c4acf9..00000000 --- a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/EnableTelegramBots.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.telegram.telegrambots.starter; - -import org.springframework.context.annotation.Import; - -/** - * Imports configuration #TelegramBotStarterConfiguration in spring context. - */ -@Import(TelegramBotStarterConfiguration.class) -public @interface EnableTelegramBots { -} diff --git a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotInitializer.java b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotInitializer.java new file mode 100644 index 00000000..5348ae7c --- /dev/null +++ b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotInitializer.java @@ -0,0 +1,45 @@ +package org.telegram.telegrambots.starter; + +import java.util.List; +import java.util.Objects; + +import org.springframework.beans.factory.InitializingBean; +import org.telegram.telegrambots.TelegramBotsApi; +import org.telegram.telegrambots.exceptions.TelegramApiException; +import org.telegram.telegrambots.generics.LongPollingBot; +import org.telegram.telegrambots.generics.WebhookBot; + +/** + * Receives all beand which are #LongPollingBot and #WebhookBot and register them in #TelegramBotsApi. + */ +public class TelegramBotInitializer implements InitializingBean { + + private final TelegramBotsApi telegramBotsApi; + private final List longPollingBots; + private final List webHookBots; + + public TelegramBotInitializer(TelegramBotsApi telegramBotsApi, + List longPollingBots, + List webHookBots) { + Objects.requireNonNull(telegramBotsApi); + Objects.requireNonNull(longPollingBots); + Objects.requireNonNull(webHookBots); + this.telegramBotsApi = telegramBotsApi; + this.longPollingBots = longPollingBots; + this.webHookBots = webHookBots; + } + + @Override + public void afterPropertiesSet() throws Exception { + try { + for (LongPollingBot bot : longPollingBots) { + telegramBotsApi.registerBot(bot); + } + for (WebhookBot bot : webHookBots) { + telegramBotsApi.registerBot(bot); + } + } catch (TelegramApiException e) { + throw new RuntimeException(e); + } + } +} diff --git a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java index 11e66af3..86ef0c33 100644 --- a/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java +++ b/telegrambots-spring-boot-starter/src/main/java/org/telegram/telegrambots/starter/TelegramBotStarterConfiguration.java @@ -1,70 +1,37 @@ package org.telegram.telegrambots.starter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.telegram.telegrambots.TelegramBotsApi; -import org.telegram.telegrambots.exceptions.TelegramApiRequestException; -import org.telegram.telegrambots.generics.LongPollingBot; -import org.telegram.telegrambots.generics.WebhookBot; - -import javax.annotation.PostConstruct; +import java.util.Collections; import java.util.List; import java.util.Optional; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.telegram.telegrambots.TelegramBotsApi; +import org.telegram.telegrambots.generics.LongPollingBot; +import org.telegram.telegrambots.generics.WebhookBot; + /** - * Receives all beand which are #LongPollingBot and #WebhookBot and register them in #TelegramBotsApi. * #TelegramBotsApi added to spring context as well */ @Configuration +@ConditionalOnProperty(prefix="telegrambots",name = "enabled", havingValue = "true", matchIfMissing = true) public class TelegramBotStarterConfiguration { - - - private final List longPollingBots; - private final List webHookBots; - - private TelegramBotsApi telegramBotsApi; - - @Autowired - public void setTelegramBotsApi(TelegramBotsApi telegramBotsApi) { - this.telegramBotsApi = telegramBotsApi; - } - - public TelegramBotStarterConfiguration(@Autowired(required = false) List longPollingBots, - @Autowired(required = false) List webHookBots) { - - this.longPollingBots = longPollingBots; - this.webHookBots = webHookBots; - } - - @PostConstruct - public void registerBots() { - Optional.ofNullable(longPollingBots).ifPresent(bots -> { - bots.forEach(bot -> { - try { - telegramBotsApi.registerBot(bot); - } catch (TelegramApiRequestException e) { - throw new RuntimeException(e); - } - }); - }); - - Optional.ofNullable(webHookBots).ifPresent(bots -> { - bots.forEach(bot -> { - try { - telegramBotsApi.registerBot(bot); - } catch (TelegramApiRequestException e) { - throw new RuntimeException(e); - } - }); - }); - } - - + @Bean @ConditionalOnMissingBean(TelegramBotsApi.class) - public TelegramBotsApi telegramBotsApi() { + public TelegramBotsApi telegramBotsApi() { return new TelegramBotsApi(); } + + @Bean + @ConditionalOnMissingBean + public TelegramBotInitializer telegramBotInitializer(TelegramBotsApi telegramBotsApi, + Optional> longPollingBots, + Optional> webHookBots) { + return new TelegramBotInitializer(telegramBotsApi, + longPollingBots.orElseGet(Collections::emptyList), + webHookBots.orElseGet(Collections::emptyList)); + } } diff --git a/telegrambots-spring-boot-starter/src/main/resources/META-INF/spring.factories b/telegrambots-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..71b3a84d --- /dev/null +++ b/telegrambots-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.telegram.telegrambots.starter.TelegramBotStarterConfiguration \ No newline at end of file diff --git a/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java b/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java index 00a1e7cf..d8723664 100644 --- a/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java +++ b/telegrambots-spring-boot-starter/src/test/java/org/telegram/telegrambots/starter/TestTelegramBotStarterConfiguration.java @@ -1,66 +1,99 @@ package org.telegram.telegrambots.starter; -import com.google.common.collect.Lists; -import org.junit.Rule; + +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.Test; -import org.mockito.Answers; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; +import static org.mockito.Mockito.*; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.TelegramBotsApi; -import org.telegram.telegrambots.exceptions.TelegramApiRequestException; import org.telegram.telegrambots.generics.LongPollingBot; import org.telegram.telegrambots.generics.WebhookBot; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - public class TestTelegramBotStarterConfiguration { - @Mock - private TelegramBotsApi telegramBotsApi; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MockTelegramBotsApi.class, TelegramBotStarterConfiguration.class)); - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Test + public void createMockTelegramBotsApiWithDefaultSettings() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(TelegramBotsApi.class); + assertThat(context).hasSingleBean(TelegramBotInitializer.class); + assertThat(context).doesNotHaveBean(LongPollingBot.class); + assertThat(context).doesNotHaveBean(WebhookBot.class); + verifyNoMoreInteractions(context.getBean(TelegramBotsApi.class)); + }); + } + + @Test + public void createOnlyLongPollingBot() { + this.contextRunner.withUserConfiguration(LongPollingBotConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(LongPollingBot.class); + assertThat(context).doesNotHaveBean(WebhookBot.class); + + TelegramBotsApi telegramBotsApi = context.getBean(TelegramBotsApi.class); + + verify(telegramBotsApi, times(1)).registerBot( context.getBean(LongPollingBot.class) ); + verifyNoMoreInteractions(telegramBotsApi); + }); + } + + @Test + public void createOnlyWebhookBot() { + this.contextRunner.withUserConfiguration(WebhookBotConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(WebhookBot.class); + assertThat(context).doesNotHaveBean(LongPollingBot.class); + + TelegramBotsApi telegramBotsApi = context.getBean(TelegramBotsApi.class); + + verify(telegramBotsApi, times(1)).registerBot( context.getBean(WebhookBot.class) ); + verifyNoMoreInteractions(telegramBotsApi); + }); + } + + @Test + public void createLongPoolingBotAndWebhookBot() { + this.contextRunner.withUserConfiguration(LongPollingBotConfig.class, WebhookBotConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(LongPollingBot.class); + assertThat(context).hasSingleBean(WebhookBot.class); - @Test - public void TestRegisterBotsWithLongPollingBots() throws TelegramApiRequestException { - when(telegramBotsApi.registerBot(any(LongPollingBot.class))).then(Answers.RETURNS_MOCKS.get()); - LongPollingBot longPollingBot = mock(LongPollingBot.class); - TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(Lists.newArrayList(longPollingBot), null); - configuration.setTelegramBotsApi(telegramBotsApi); + TelegramBotsApi telegramBotsApi = context.getBean(TelegramBotsApi.class); + + verify(telegramBotsApi, times(1)).registerBot( context.getBean(LongPollingBot.class) ); + verify(telegramBotsApi, times(1)).registerBot( context.getBean(WebhookBot.class) ); + //verifyNoMoreInteractions(telegramBotsApi); + }); + } - configuration.registerBots(); + @Configuration + static class MockTelegramBotsApi{ - verify(telegramBotsApi, times(1)).registerBot(longPollingBot); - verifyNoMoreInteractions(telegramBotsApi); - } - - @Test - public void TestRegisterBotsWithWebhookBots() throws TelegramApiRequestException { - doNothing().when(telegramBotsApi).registerBot(any(WebhookBot.class)); - WebhookBot webhookBot = mock(WebhookBot.class); - TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(null, Lists.newArrayList(webhookBot)); - configuration.setTelegramBotsApi(telegramBotsApi); - - configuration.registerBots(); - - verify(telegramBotsApi, times(1)).registerBot(webhookBot); - verifyNoMoreInteractions(telegramBotsApi); - } - - @Test - public void TestRegisterBotsWithLongPollingBotsAndWebhookBots() throws TelegramApiRequestException { - doNothing().when(telegramBotsApi).registerBot(any(WebhookBot.class)); - LongPollingBot longPollingBot = mock(LongPollingBot.class); - WebhookBot webhookBot = mock(WebhookBot.class); - TelegramBotStarterConfiguration configuration = new TelegramBotStarterConfiguration(Lists.newArrayList(longPollingBot), Lists.newArrayList(webhookBot)); - configuration.setTelegramBotsApi(telegramBotsApi); - - configuration.registerBots(); - - verify(telegramBotsApi, times(1)).registerBot(longPollingBot); - verify(telegramBotsApi, times(1)).registerBot(webhookBot); - verifyNoMoreInteractions(telegramBotsApi); - } + @Bean + public TelegramBotsApi telegramBotsApi() { + return mock(TelegramBotsApi.class); + } + } + + @Configuration + static class LongPollingBotConfig{ + @Bean + public LongPollingBot longPollingBot() { + return mock(LongPollingBot.class); + } + } + + @Configuration + static class WebhookBotConfig{ + @Bean + public WebhookBot webhookBot() { + return mock(WebhookBot.class); + } + } } From 52d385fe8669748bb3adce21431438348448a06e Mon Sep 17 00:00:00 2001 From: Ruben Bermudez Date: Thu, 21 Jun 2018 16:22:34 +0200 Subject: [PATCH 40/41] Add files via upload --- jetbrains.png | Bin 0 -> 181839 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 jetbrains.png diff --git a/jetbrains.png b/jetbrains.png new file mode 100644 index 0000000000000000000000000000000000000000..150246099be31b4d86bbc3e5f6fecda202af809b GIT binary patch literal 181839 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ĩ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

    `}UL=(0{IZgFB1swu^;+YaoL@%)bYd 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

    c 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 z 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 Date: Thu, 21 Jun 2018 16:41:16 +0200 Subject: [PATCH 41/41] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index e9feeea2..e30c31a1 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,12 @@ This library use [Telegram bot API](https://core.telegram.org/bots), you can fin ## Questions or Suggestions Feel free to create issues [here](https://github.com/rubenlagus/TelegramBots/issues) as you need or join the [chat](https://telegram.me/JavaBotsApi) +## Powered by Intellij +

    + +

    + + ## License MIT License