From 673506417c42fbc0e23fbb8232306c1b139f8fa6 Mon Sep 17 00:00:00 2001 From: Abbas Abou Daya Date: Mon, 30 Oct 2017 20:45:43 -0400 Subject: [PATCH] Write tutorial and implement SilentTest --- TelegramBots.wiki/Getting-Started.md | 2 +- TelegramBots.wiki/_Sidebar.md | 10 +- .../abilities/Bot-Administration.md | 0 TelegramBots.wiki/abilities/Bot-Recovery.md | 8 + TelegramBots.wiki/abilities/Bot-Testing.md | 172 ++++++++++++++++++ .../abilities/Database-Handling.md | 57 ++++++ TelegramBots.wiki/abilities/Hello-Ability.md | 109 +++++++++++ TelegramBots.wiki/abilities/Simple-Example.md | 117 ++++++++++++ TelegramBots.wiki/abilities/Using-Replies.md | 77 ++++++++ .../abilitybots/api/sender/DefaultSender.java | 1 - .../abilitybots/api/sender/SilentSender.java | 46 ++--- .../abilitybots/api/bot/DefaultBot.java | 2 - .../api/sender/SilentSenderTest.java | 43 +++++ 13 files changed, 618 insertions(+), 26 deletions(-) create mode 100644 TelegramBots.wiki/abilities/Bot-Administration.md create mode 100644 TelegramBots.wiki/abilities/Bot-Recovery.md create mode 100644 TelegramBots.wiki/abilities/Bot-Testing.md create mode 100644 TelegramBots.wiki/abilities/Database-Handling.md create mode 100644 TelegramBots.wiki/abilities/Hello-Ability.md create mode 100644 TelegramBots.wiki/abilities/Simple-Example.md create mode 100644 TelegramBots.wiki/abilities/Using-Replies.md create mode 100644 telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java diff --git a/TelegramBots.wiki/Getting-Started.md b/TelegramBots.wiki/Getting-Started.md index 179577fb..e1b7b0f3 100644 --- a/TelegramBots.wiki/Getting-Started.md +++ b/TelegramBots.wiki/Getting-Started.md @@ -88,7 +88,7 @@ Now that we have the library, we can start coding. There are few steps to follow .setChatId(update.getMessage().getChatId()) .setText(update.getMessage().getText()); try { - sendMessage(message); // Call method to send the message + execute(message); // Call method to send the message } catch (TelegramApiException e) { e.printStackTrace(); } diff --git a/TelegramBots.wiki/_Sidebar.md b/TelegramBots.wiki/_Sidebar.md index 132020b0..a253fc21 100644 --- a/TelegramBots.wiki/_Sidebar.md +++ b/TelegramBots.wiki/_Sidebar.md @@ -2,5 +2,13 @@ * [[Getting Started]] * [[Errors Handling]] * [[FAQ]] +* AbilityBot + * [Simple Example](abilities/Simple-Example.md) + * [Hello Ability](abilities/Hello-Ability.md) + * [Using Replies](abilities/Using-Replies.md) + * [Database Handling](abilities/Database-Handling.md) + * [Bot Testing](abilities/Bot-Testing.md) + * [Bot Recovery](abilities/Bot-Recovery.md) + * [Bot Administration](abilities/Bot-Administration.md) * [[Changelog]] - * [[How To Update]] + * [[How To Update]] \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Bot-Administration.md b/TelegramBots.wiki/abilities/Bot-Administration.md new file mode 100644 index 00000000..e69de29b diff --git a/TelegramBots.wiki/abilities/Bot-Recovery.md b/TelegramBots.wiki/abilities/Bot-Recovery.md new file mode 100644 index 00000000..4796b2a3 --- /dev/null +++ b/TelegramBots.wiki/abilities/Bot-Recovery.md @@ -0,0 +1,8 @@ +# Bot Recovery +With recovery, we specifically mean recovering the DB in-case of false data being committed. This is a neat feature supported by DBContext, you can /backup and /recover your bot whenever needed. + +Once you /backup, the bot will respond back with a valid JSON object that represents all the data in the DB. + +On /recover, the bot will ask for the JSON file. A reply to the message with the file attached will recover the bot with the previous state DB. + +Try to experiment using the counter ability introduced in [Database Handling](Database-Handling.md)! \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Bot-Testing.md b/TelegramBots.wiki/abilities/Bot-Testing.md new file mode 100644 index 00000000..a0b3d089 --- /dev/null +++ b/TelegramBots.wiki/abilities/Bot-Testing.md @@ -0,0 +1,172 @@ +# Testing +It is super important to be able to test your bot prior to "release". In this case, release would mean that you're presenting the bot to your designated audience. Nobody likes bots that are buggy, faulty and do clumsy actions. +As developers, we appreciate frameworks that provide an ease in testing. Of course, you might no tbe able to catch all bugs that can occur in production, but you'd be far more comfortable in releasing a bot that is well-tested. + +## Limitations + +The issue with the basic API is that all DefaultAbsSender methods (the bot methods you use to send message) are statically defined without interfacing. If you declare your bot and try to do some testing, you won't be able to know that you've executed a method... unless you actually execute it! As an example: +```java +public void sayHello() { + SendMessage snd = new SendMessage(); + snd.setText("Hello!"); + snd.setChatId(123); + + try { + // We want to test that we actually sent out this message with the contents "Hello!" + execute(snd); + } catch (TelegramApiException e) {} +} +``` + +This is how you would define a method that says hello in the basic API. How do you go around testing it? If you do attempt to Junit test this method, what will you be testing? If change the method signature to return the string sent, then you can test the hello message content. However, can you test that you've actually `executed` the command? + +## Mock Testing +*This section assumes you're familiar with mock testing. Mock testing is basically replacing a real object X with a fake object Y (a mock) of the same type. By doing that, you're able to test whether certain functions were executed.* + +Obviously, you can't, but there's a twist to it. You can always mock the whole bot, but with that you're also mocking the method `sayHello` when you actually need its contents and code! We need to extract the bot-sending-specific-methods into their own interface and try to mock that interface instead. + +## MessageSender Interface +All ability bots declare two utility objects. +### The Sender Object +The sender object is an implementation of the [MessageSender](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/MessageSender.java) interface. The interface mirrors +all the bot sending methods. A user can supply his own MessageSender, but the AbilityBot module specifies a [DefaultSender](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/DefaultSender.java) As you might guess, the default sender is simply a proxy for the bot API methods. + + ## AbilityBot Testing + Let's suppose that you have an ability that says "Hello World!" declared as such: + ```java +public Ability saysHelloWorld() { + return Ability.builder() + .name("hello") + .info("Says hello world!") + .privacy(PUBLIC) + .locality(ALL) + .action(ctx -> { + try{ + sender.execute(new SendMessage().setChatId(ctx.getChatId()).setText("Hello World!")); + } catch (TelgramApiException e){} + }) + .build(); + } +``` + +The test for this ability would be: + +```java +@Test + public void canSayHelloWorld() { + Update upd = new Update(); + // Create a new EndUser - EndUser is a class similar to Telegram User, but contains + // some utility methods like fullName() and shortName() for ease of use + EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37"); + // This is the context that you're used to, it is the necessary conumer item for the ability + MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID); + + // We consume a context in the lamda declaration, so we pass the context to the action logic + bot.saysHelloWorld().action().accept(context); + + // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID + // The sender here is a mock! + Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); + } +``` + +The comments explain every step in the test. In a single assertion with Mockito, we assert that: +* We've sent the message once +* The message content was "Hello World!" +* The message was sent to a specific chat ID + +There are some preparations involved before we can perform such a test. Here's the full code snippet for running this test: +```java +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.telegram.abilitybots.api.db.DBContext; +import org.telegram.abilitybots.api.db.MapDBContext; +import org.telegram.abilitybots.api.objects.EndUser; +import org.telegram.abilitybots.api.objects.MessageContext; +import org.telegram.abilitybots.api.sender.MessageSender; +import org.telegram.telegrambots.api.objects.Update; + +import static org.mockito.Mockito.*; + +public class ExampleBotTest { + public static final int USER_ID = 1337; + public static final long CHAT_ID = 1337L; + + // Your bot handle here + private ExampleBot bot; + // Your sender here + private MessageSender sender; + + @Before + public void setUp() { + // Create your bot + bot = new ExampleBot(); + // Create a new sender as a mock + sender = mock(MessageSender.class); + // Set your bot sender to the mocked sender + // THIS is the line that prevents your bot from communicating with Telegram servers when it's running its own abilities + // All method calls will go through the mocked interface -> which would do nothing except logging the fact that you've called this function with the specific arguments + bot.sender = sender; + } + + @Test + public void canSayHelloWorld() { + Update upd = new Update(); + // Create a new EndUser - EndUser is a class similar to Telegram User, but contains + // some utility methods like fullName() and shortName() for ease of use + EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37"); + // This is the context that you're used to, it is the necessary conumer item for the ability + MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID); + + // We consume a context in the lamda declaration, so we pass the context to the action logic + bot.saysHelloWorld().action().accept(context); + + // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID + // The sender here is a mock! + Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); + } +} +``` + +## DB Abilities +What if the ability performs a DB interaction? We don't want testing procedures to modify the database of the bot. + +This is where we differentiate between an online DB and an offline DB. The online DB is the default DB when the bot is instantiated. However, AbilityBot supplies a constructor that reveals a DBContext argument. We can supply another instance of a DB (an offline one) so that the tests don't modify our online DB. + +In ExampleBot, we do: +```java + public ExampleBot(DBContext db) { + super(BOT_TOKEN, BOT_USERNAME, db); + } +``` + +In ExampleBotTest: +```java +public class ExampleBotTest { + ... + + private DBContext db; + private MessageSender sender; + +@Before + public void setUp() { + // Offline instance will get deleted at JVM shutdown + db = MapDBContext.offlineInstance("test"); + bot = new ExampleBot(db); + + ... + } + ... + + // We should clear the DB after every test as such + @After + public void tearDown() { + db.clear(); + } +} +``` + +## Silent Testing +As mentioned before, we also have another object that is able to send messages silently called `silent`. If you're using this object, you will also need to mock it and use it in the test. It is handled in exactly the same way. \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Database-Handling.md b/TelegramBots.wiki/abilities/Database-Handling.md new file mode 100644 index 00000000..cd6fa6f9 --- /dev/null +++ b/TelegramBots.wiki/abilities/Database-Handling.md @@ -0,0 +1,57 @@ +#Database Handling +AbilityBots come with an embedded DB. Users are free to implement their own databases via implementing the [DBContext](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java) class. +The abstraction has multiple constructors to accommodate user-defined implementations of [DBContext](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java) and [MessageSender](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/MessageSender.java). We'll talk about the message sender interface in the [bot testing](Bot-Testing.md) section. + +##Example +We'll be introducing an ability that maintains a special counter for every user. At every /increment, the user will receive a message with the previous number + 1. We'll initially start from zero and increment onwards. + +```java + /** + * Use the database to fetch a count per user and increments. + *

+ * Use /count to experiment with this ability. + */ + public Ability useDatabaseToCountPerUser() { + return Ability.builder() + .name("count") + .info("Increments a counter per user") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> { + // db.getMap takes in a string, this must be unique and the same everytime you want to call the exact same map + // TODO: Using integer as a key in this db map is not recommended, it won't be serialized/deserialized properly if you ever decide to recover/backup db + Map countMap = db.getMap("COUNTERS"); + int userId = ctx.user().id(); + + // Get and increment counter, put it back in the map + Integer counter = countMap.compute(String.valueOf(userId), (id, count) -> count == null ? 1 : ++count); + + /* + + Without lambdas implementation of incrementing + + int counter; + if (countMap.containsKey(userId)) + counter = countMap.get(userId) + 1; + else + counter = 1; + countMap.put(userId, counter); + + */ + + // Send formatted will enable markdown + String message = String.format("%s, your count is now *%d*!", ctx.user().shortName(), counter); + silent.send(message, ctx.chatId()); + }) + .build(); + } +``` + +After successfully adding that ability to your bot, try to /count and watch as the number increases at every message. +Other important functions in the `db` object: +* getSet - gets a set of data +* getList - gets a list of data +* summary - gets a summary of the present structs + +Be sure to check out [DBContext](../../telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/db/DBContext.java) for all the implemented methods. \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Hello-Ability.md b/TelegramBots.wiki/abilities/Hello-Ability.md new file mode 100644 index 00000000..75f99fb5 --- /dev/null +++ b/TelegramBots.wiki/abilities/Hello-Ability.md @@ -0,0 +1,109 @@ +Motivation +---------- +After implementing your own bot via the basic API, you'll quickly realize how verbose it is. Once you get multiple commands up and running, your routing logic and handling per command start to become cumbersome. +Dealing with a basic API has its advantages and disadvantages. Obviously, there's nothing hidden. If it's there on Telegram, it's here in the Java API. However, can we do better than just a basic API? + +When you want to implement a feature in your bot, you start asking these questions: + +* The **WHO**? + * Who is going to use this feature? Should they be allowed to use all the features? +* The **WHAT**? + * Under what conditions should I allow this feature? + * Should the message have a photo? A document? Oh, maybe a callback query? +* The **HOW**? + * If my bot crashes, how can I resume my operation? + * Should I utilize a DB? + * How can I separate logic execution of different features? + * How can I unit-test my feature outside of Telegram? + +Every time you write a command or a feature, you will need to answer these questions and ensure that your feature logic works. + +Ability +---------------------- +Simply put, the abilities module was developed to make your life easier. Whether you're counting numbers, fetching images or handling large data, AbilityBot is here to augment your development. + +The AbilityBot abstraction intends to provide the following: +* New feature is a new **Ability**, a new method - no fuss, zero overhead, no cross-code with other features +* Argument length on a command is as easy as changing a single integer +* Privacy settings per Ability - access levels to Abilities! User | Admin | Creator +* Embedded database - available for every declared ability +* Proxy sender interface - enhances testability; accurate results pre-release + +Alongside these exciting core features of the AbilityBot, the following have been introduced: +* The bot automatically maintains an up-to-date set of all the users who have contacted the bot +* Backup and recovery for the DB +* Ban and unban users from accessing your bots +* Promote and demote users as bot administrators + +Abstraction +-------------- + +The AbilityBot abstraction defines a new object, named **Ability**. An ability combines conditions, flags, action, post-action and replies. +As an example, here is a code-snippet of an ability that creates a ***/hello*** command: + +```java +public Ability sayHelloWorld() { + return Ability + .builder() + .name("hello") + .info("says hello world!") + .input(0) + .locality(USER) + .privacy(ADMIN) + .action(ctx -> silent.send("Hello world!", ctx.chatId())) + .post(ctx -> silent.send("Bye world!", ctx.chatId())) + .build(); +} +``` +Here is a breakdown of the above code snippet: +* *.name()* - the name of the ability (essentially, this is the command) +* *.info()* - provides information for the command + * More on this later, but it basically centralizes command information in-code. +* *.input()* - the number of input arguments needed, 0 is for do-not-care +* *.locality()* - this answers where you want the ability to be available + * In GROUP, USER private chats or ALL (both) +* *.privacy()* - this answers who you want to access your ability + * CREATOR, ADMIN, or everyone as PUBLIC +* *.action()* - the feature logic resides here (a lambda function that takes a *MessageContext*) + * *MessageContext* provides fast accessors for the **chatId**, **user** and the underlying **update**. It also conforms to the specifications of the basic API. +* *.post()* - the logic executed **after** your main action finishes execution + +The `silent` object is created with every AbilityBot. It provides helper and utility functions that execute "silently". In this context, silent execution of bot API methods are ones that don't throw an exception. However, all methods in silent return an Optional object. If an exception occurs, the optional would be empty. The developer would still be able to +manage errors by checking for the presence of the optional `.isPresent()`. This decreases verboseness while still being able to execute routines correctly. +Do note that: +* You can still access the bot's methods and functions inside the lambda function in your action definition. That includes all the DefaultAbsSender methods execute, executeAsync, setChatPhoto, etc.... +* `silent` uses another accessible object named `sender`. Refer to [[Bot Testing]] for the main use case of sender as an interface to all bot methods. + +With abilities, you can specify the context of the feature. If you only want the command to be available for groups, then you can set `.locality(GROUP)`. If it is a very sensitive command that only admins should have access to, then set `.privacy(ADMIN)`. +This allows for abilities with protection guarantees on who can use and where it can be used. + +The following is a snippet of how this would look like with the plain basic API. +```java + @Override + public void onUpdateReceived(Update update) { + // Global checks... + // Switch, if, logic to route to hello world method + // Execute method + } + + public void sayHelloWorld(Update update) { + if (!update.hasMessage() || !update.getMessage().isUserMessage() || !update.getMessage().hasText() || update.getMessage.getText().isEmpty()) + return; + User maybeAdmin = update.getMessage().getFrom(); + /* Query DB for if the user is an admin, can be SQL, Reddis, Ignite, etc... + If user is not an admin, then return here. + */ + + SendMessage snd = new SendMessage(); + snd.setChatId(update.getMessage().getChatId()); + snd.setText("Hello world!"); + + try { + execute(snd); + } catch (TelegramApiException e) { + BotLogger.error("Could not send message", TAG, e); + } + } +``` + +I will leave you the choice to decide between the two snippets as to which is more **readable**, **writable** and **testable**. \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Simple-Example.md b/TelegramBots.wiki/abilities/Simple-Example.md new file mode 100644 index 00000000..9462d30c --- /dev/null +++ b/TelegramBots.wiki/abilities/Simple-Example.md @@ -0,0 +1,117 @@ +#AbilityBot +This section of the tutorial will present a barebone example on creating your first AbilityBot! It is highly recommended to write your very first bot via the [simple users guide](./Simple-Example.md). That will give you a sense of how the basic API allows you to handle commands and features. + +##Dependencies +As with any Java project, you will need to set your dependencies. + +* **Maven** +```xml + + org.telegram + telegrambots-abilties + 3.4 + +``` +* **Gradle** +```groovy + compile group: 'org.telegram', name: 'telegrambots-abilties', version: '3.4' +``` +* [JitPack](https://jitpack.io/#rubenlagus/TelegramBots) + +* [Plain Imports/Jars](https://github.com/rubenlagus/TelegramBots/releases) + +##Bot Declaration +To use the abilities module, you will need to extend AbilityBot. +```java +import org.telegram.abilitybots.api.bot.AbilityBot; + +public class HelloBot extends AbilityBot { + ... +} +``` + +##Bot Implementation +Bot token and nickname are passed via the constructor and don't require an override. +```java + public HelloBot(String token, String username) { + super(token, username); + } +``` + +However, since the token and username of a bot are usually constants, you can do the following: +```java +public static String BOT_TOKEN = "..."; +public static String BOT_USERNAME = "..."; + + public HelloBot() { + super(BOT_TOKEN, BOT_USERNAME); + } +``` + +AbilityBot forces a single implementation of creator ID. This ID corresponds to you, the bot developer. The bot needs to know its master since it has sensitive commands that only the master can use. +So, if your Telegram ID Is 123456789, then add the following method: +```java + @Override + public int creatorId() { + return 123456789; + } +``` + +That's it to have a valid, compilable and ready to be deployed bot. However, your bot doesn't have a single command to use. Let's declare one! + +##Hello Ability +To add a feature to your bot, you add an ability. That's it! No routing from onUpdateReceived, no separate checks and no crossovers. Let's write our first ability that simply says hello! + +```java +public Ability sayHelloWorld() { + return Ability + .builder() + .name("hello") + .info("says hello world!") + .locality(ALL) + .privacy(PUBLIC) + .action(ctx -> silent.send("Hello world!", ctx.chatId())) + .build(); +} +``` + +Save your questions for later! Abilities are described in detail in the following sections of the tutorial. +## Running Your Bot +Running the bot is just like running the regular Telegram bots. Create a Java class similar to the one below. +```java +public class Application { + public static void main(String[] args) { + // Initializes dependencies necessary for the base bot - Guice + ApiContextInitializer.init(); + + // Create the TelegramBotsApi object to register your bots + TelegramBotsApi botsApi = new TelegramBotsApi(); + + try { + // Register your newly created AbilityBot + botsApi.registerBot(new HelloBot()); + } catch (TelegramApiException e) { + e.printStackTrace(); + } + } +} +``` + +If you're in doubt that you're missing some code, the full code example can be inspected [Here]() +## Testing Your Bot +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. +* /claim - Claims this bot +* /backup - returns a backup of the bot database +* /recover - recovers the database +* /promote @username - promotes user to bot admin +* /demote @username - demotes bot admin to user +* /ban @username - bans the user from accessing your bot commands and features +* /unban @username - lifts the ban from the user + +## Conclusion +Congratulation on creating your first AbilityBot. What's next? So far we've only considered the case of commands, but what about images and inline replies? AbilityBots can also handle that! Oh and, did you know that all ability bots have an embedded database that you can use? +The following sections of the tutorial will describe in detail **abilities** and **replies**. It will also bring into attention how to effectively in-code test your bot, handle the embedded DB and administer your user access levels. \ No newline at end of file diff --git a/TelegramBots.wiki/abilities/Using-Replies.md b/TelegramBots.wiki/abilities/Using-Replies.md new file mode 100644 index 00000000..37883be9 --- /dev/null +++ b/TelegramBots.wiki/abilities/Using-Replies.md @@ -0,0 +1,77 @@ +#Replies + +A reply is AbilityBot's swiss army knife. It comes in two variants and is able to handle all possible use cases. + +##Standalone Reply +Standalone replies are replies declared on their own without being attached to an ability. Here's an example of a possible reply declaration: +```java +/** +* A reply that says "yuck" to all images sent to the bot. +*/ +public Reply sayYuckOnImage() { + // getChatId is a public utility function in rg.telegram.abilitybots.api.util.AbilityUtils + Consumer action = upd -> silent.send("Yuck", getChatId(upd)); + + return Reply.of(upd, Flag.PHOTO) +} +``` + +Let's break this down. Replies require a lambda function (consumer) that is able to consume our update. In this case, our consumer simply fetches the chatId +from the update and sends a "Yuck" message. `Reply.of(upd)` would be enough. However, replies accept a var-arg of type `Predicate`. These predicates are the necessary conditions so that the bot acts the reply. We specify Flag.PHOTO to let the bot know + that we only want the reply to act on images only! The Flag is a public enum at your disposal. It contains other conditionals like checking for videos, messages, voice, documents, etc... + +##Ability Reply +In exactly the same manner, you are able to attach replies to abilities. This way you can localize replies that relate to the same ability. +```java +public Ability playWithMe() { + String playMessage = "Play with me!"; + + return Ability.builder() + .name("play") + .info("Do you want to play with me?") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> sender.forceReply(playMessage, ctx.chatId())) + // The signature of a reply is -> (Consumer action, Predicate... conditions) + // So, we first declare the action that takes an update (NOT A MESSAGECONTEXT) like the action above + // The reason of that is that a reply can be so versatile depending on the message, context becomes an inefficient wrapping + .reply(upd -> { + // Prints to console + System.out.println("I'm in a reply!"); + // Sends message + sender.send("It's been nice playing with you!", upd.getMessage().getChatId()); + }, + // Now we start declaring conditions, MESSAGE is a member of the enum Flag class + // That class contains out-of-the-box predicates for your replies! + // MESSAGE means that the update must have a message + // This is imported statically, Flag.MESSAGE + MESSAGE, + // REPLY means that the update must be a reply, Flag.REPLY + REPLY, + // A new predicate user-defined + // The reply must be to the bot + isReplyToBot(), + // If we process similar logic in other abilities, then we have to make this reply specific to this message + // The reply is to the playMessage + isReplyToMessage(playMessage) + ) + // You can add more replies by calling .reply(...) + .build(); + } + + private Predicate isReplyToMessage(String message) { + return upd -> { + Message reply = upd.getMessage().getReplyToMessage(); + return reply.hasText() && reply.getText().equalsIgnoreCase(message); + }; + } + + private Predicate isReplyToBot() { + return upd -> upd.getMessage().getReplyToMessage().getFrom().getUserName().equalsIgnoreCase(getBotUsername()); + } +``` + +In this example, we showcase how we can supply our own predicates. The two new predicates are `isReplyToMessage` and `isReplyToBot`. +The checks are made so that, once you execute your logic there is not need to check for the validity of the reply. +They were all made once the action logic is being executed. \ No newline at end of file diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/DefaultSender.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/DefaultSender.java index 0e62c6ab..4ea2d59a 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/DefaultSender.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/DefaultSender.java @@ -1,6 +1,5 @@ package org.telegram.abilitybots.api.sender; -import org.telegram.telegrambots.api.methods.AnswerInlineQuery; import org.telegram.telegrambots.api.methods.BotApiMethod; import org.telegram.telegrambots.api.methods.groupadministration.SetChatPhoto; import org.telegram.telegrambots.api.methods.send.*; diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java index ed6f2004..4b72f705 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/sender/SilentSender.java @@ -10,7 +10,12 @@ import org.telegram.telegrambots.logging.BotLogger; import java.io.Serializable; import java.util.Optional; -//TODO: DOC + +/** + * A silent sender that returns {@link Optional} objects upon execution. Mainly used to decrease verboseness of exception handling. + * + * @author Abbas Abou Daya + */ public class SilentSender { private static final String TAG = SilentSender.class.getSimpleName(); @@ -37,6 +42,24 @@ public class SilentSender { return execute(msg); } + public > Optional execute(Method method) { + try { + return Optional.ofNullable(sender.execute(method)); + } catch (TelegramApiException e) { + BotLogger.error("Could not execute bot API method", TAG, e); + return Optional.empty(); + } + } + + public > Optional executeAsync(Method method) { + try { + return Optional.ofNullable(sender.execute(method)); + } catch (TelegramApiException e) { + BotLogger.error("Could not execute bot API method", TAG, e); + return Optional.empty(); + } + } + private Optional doSendMessage(String txt, long groupId, boolean format) { SendMessage smsg = new SendMessage(); smsg.setChatId(groupId); @@ -45,23 +68,4 @@ public class SilentSender { return execute(smsg); } - - private > Optional execute(Method method) { - try { - return Optional.ofNullable(sender.execute(method)); - } catch (TelegramApiException e) { - BotLogger.error("Could not execute bot API method", TAG, e); - return Optional.empty(); - } - } - - private > Optional executeAsync(Method method) { - try { - return Optional.ofNullable(sender.execute(method)); - } catch (TelegramApiException e) { - BotLogger.error("Could not execute bot API method", TAG, e); - return Optional.empty(); - } - - } -} +} \ No newline at end of file diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java index 34b8fb00..c6f843d7 100644 --- a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/DefaultBot.java @@ -1,10 +1,8 @@ package org.telegram.abilitybots.api.bot; -import com.google.common.annotations.VisibleForTesting; import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.objects.Ability; import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder; -import org.telegram.abilitybots.api.sender.MessageSender; import static org.telegram.abilitybots.api.objects.Ability.builder; import static org.telegram.abilitybots.api.objects.Flag.CALLBACK_QUERY; diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java new file mode 100644 index 00000000..1fb46a55 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/sender/SilentSenderTest.java @@ -0,0 +1,43 @@ +package org.telegram.abilitybots.api.sender; + +import org.junit.Before; +import org.junit.Test; +import org.telegram.telegrambots.exceptions.TelegramApiException; + +import java.util.Optional; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SilentSenderTest { + private SilentSender silent; + private MessageSender sender; + + @Before + public void setUp() { + sender = mock(MessageSender.class); + silent = new SilentSender(sender); + } + + @Test + public void returnsEmptyOnError() throws TelegramApiException { + when(sender.execute(any())).thenThrow(TelegramApiException.class); + + Optional execute = silent.execute(null); + + assertFalse("Execution of a bot API method with execption results in a nonempty optional", execute.isPresent()); + } + + @Test + public void returnOptionalOnSuccess() throws TelegramApiException { + String data = "data"; + when(sender.execute(any())).thenReturn(data); + + Optional execute = silent.execute(null); + + assertEquals("Silent execution resulted in a different object", data, execute.get()); + } +} \ No newline at end of file