Getting Started with Telegrams AbilityBot

Dominik JülgBots, Java, TutorialsLeave a Comment

This getting started is for developers who want to create a telegram chatbot with Java. There are many articles on the internet explaining how to create a chatbot based on the so-called LongPollingBot. But development works much faster and easier with the AbilityBot, which will be our topic for today.

In the course of the article we are going to build a simple chatbot that reminds its users every two days to work out. The result will look something like this:

bot chat result

Even with this simple example, we can take a look at a bunch of great features provided by the Telegram API and more specific via AbilityBot. In a nutshell, those features are:

  • Replying to commands like /start
  • Using inline keyboards like in the picture above
  • making use of the embedded database in ability bot
  • and as an extra: Repeated execution of tasks at a specific time

If you don’t want to work yourself through the whole article, feel free to take a look at the finished code on github.

Okay then, let’s start coding!

Project setup

For the AbilityBot to work, you need to set up a Maven project in a Java 8 environment. In case you have never done that before, head over to Maven’s getting started and setup a new maven project. In your pom.xml add the following two dependencies:

<dependencies>
        <dependency>
                <groupId>org.telegram</groupId>
                <artifactId>telegrambots</artifactId>
                <version>3.6.1</version>
        </dependency>

        <dependency>
                <groupId>org.telegram</groupId>
                <artifactId>telegrambots-abilities</artifactId>
                <version>3.6.1</version>
        </dependency>
</dependencies>

Ensure that you are using at least Java 8 by adding the following build plugin to your pom:

<build>
        <plugins>
                <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                                <source>8</source>
                                <target>8</target>
                        </configuration>
                </plugin>
        </plugins>
</build>

Getting your bot to talk

At first, we create our bot class called FitnessBot. It extends the class AbilityBot thus we need to call super() on the constructor and override the method creatorId(). Furthermore we added another constructor without any arguments, thus we can easily instantiate our bot.

public class FitnessBot extends AbilityBot {

    public FitnessBot() {
        this(Constants.BOT_TOKEN, Constants.BOT_USERNAME);
    }

    private FitnessBot(String botToken, String botUsername) {
        super(botToken, botUsername);
    }

    @Override
    public int creatorId() {
        return Constants.CREATOR_ID;
    }
}

At the latest when you paste this code into your IDE, you will recognize the unresolved reference to an interface called Constants. It is a common software pattern widely used in Android for example. The interface contains all the constant values needed by your application to function properly. Later we will use it for all our text responses, database identifiers, and similar stuff. In order to fix your code for now, create a new interface and paste the following lines.

public interface Constants {
    // Initialization
    String BOT_USERNAME = "FitnessBot";
    String BOT_TOKEN = "your-super-secret-token";
    int CREATOR_ID = your-telegram-user-id;
}

You need to replace the information with the ones @BotFather gave to you. If you don’t know what I’m talking about please refer to this tutorial. It contains information on how to create a new Telegram bot.

Running your bot

Before you can run your bot you have to initialize it. Therefore we create a new class called Application and fill it with a main() method containing the code to initialize your bot. This block is copied from the Github documentation of ability bot.

public class Application {

    public static void main(String[] args) {
        // Initializes dependencies necessary for the base bot
        ApiContextInitializer.init();

        // Create the TelegramBotsApi object to register your bots
        TelegramBotsApi botsApi = new TelegramBotsApi();

        try {
            // Register your newly created AbilityBot
            FitnessBot bot = new FitnessBot();
            botsApi.registerBot(bot);

        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }
}

Now you can run your bot but he won’t answer, yet. In the next step, we change this by developing the first ability: A response to the command /start. Therefore, you have to add the following method to your FitnessBot class.

public Ability replyToStart() {
    return Ability
        .builder()
        .name("start")
        .info(Constants.START_DESCRIPTION)
        .locality(ALL)
        .privacy(PUBLIC)
        .action(ctx ->  silent.send("Hello World!", ctx.chatId()))
        .build();
}

In Constants add:

String START_DESCRIPTION = "Start using the fitness bot to remind you doing sports";

I don’t want to go into detail on the specific methods chained here because they are pretty well explained in the already linked documentation.

But here are some information explaining action() more thoroughly. ctx is the message context and provides related information like the chatId of the message. To reply to a message you have to call a method on either object silent or sender. Both are provided by the parent class AbilityBot. As you can see in our code, currently we are using the silent object. The difference is that while silent is used to send plain text messages only, sender gives you more freedom on how to compose your reply. As you will learn later, inline keyboards, for example, need to use sender. The downside of sender is that more freedom leads to more responsibility. Exceptions might occour and you have to handle them by yourself.

Database usage and state handling

State handling and properly saving that state to a database can get pretty complex. So it makes sense to create a separate class for that concern. Therefore we build a new class ResponseHandler which will be responsible for processing requests:

public class ResponseHandler {
    private final MessageSender sender;
    private final Map<Long, State> chatStates;

    public ResponseHandler(MessageSender sender, DBContext db) {
        this.sender = sender;
        chatStates = db.getMap(Constants.CHAT_STATES);
    }
}

In it’s constructor, the ResponseHandler receives the database context called db. The instance, which is going to be created using the constructor, will become a field in our FitnessBot class. Field sender will be used to send messages back to the user. As you can see, the state of the bot is saved separately for each chat in a Map called chatStates.

The next step is to add a new entry to our Constants interface:

String CHAT_STATES = "CHAT_STATES"

This is the name of the table created internally by the embedded database. Pretty simple. You can read and even update the map chatStates as you wish and it will be synced with the database automagically.

magic gif

If you wondered about State-class: This is an enum containing our set of states for the bot. Currently, just a single one which says that we are waiting for the user to reply.

public enum State {
    AWAITING_TRAINING_DAY
}

To make use of the whole new code we need to initialize our ResponseHandler in the bot class. We do that using a new field inside of FitnessBot.

private final ResponseHandler responseHandler;

public FitnessBot(String botToken, String botUsername) {
        super(botToken, botUsername);
        responseHandler = new ResponseHandler(sender, db);
    }

Now we need to replace the action() method in our replyToStart() ability with

.action(ctx ->  responseHandler.replyToStart(ctx.chatId()))

But our ResponseHandler doesn’t have the referenced method, so far. You can find it below.

public void replyToStart(long chatId) {
        try {
                sender.execute(new SendMessage()
                        .setText(Constants.START_REPLY)
                        .setChatId(chatId));
                chatStates.put(chatId, State.AWAITING_TRAINING_DAY);
        } catch (TelegramApiException e) {
                e.printStackTrace();
        }
}

And our text is saved in Constants:

String START_REPLY = "Welcome I'm FitnessBot. I'm here to remind you doing sports every second day!";

If you run the application now and issue /start our newly added text will appear!

Using inline keyboards

Chances are that you will use more than one inline keyboard. That’s why we create a new class called KeyboardFactory. This class will create the needed keyboard instances for us. The code is pretty self-explanatory but see for yourself.

public class KeyboardFactory {
    public static ReplyKeyboard withTodayTomorrowButtons() {
        InlineKeyboardMarkup inlineKeyboard = new InlineKeyboardMarkup();
        List<List<InlineKeyboardButton>> rowsInline = new ArrayList<>();
        List<InlineKeyboardButton> rowInline = new ArrayList<>();
        rowInline.add(new InlineKeyboardButton().setText(Constants.TRAINING_TODAY).setCallbackData(Constants.TRAINING_TODAY));
        rowInline.add(new InlineKeyboardButton().setText(Constants.TRAINING_TOMORROW).setCallbackData(Constants.TRAINING_TOMORROW));
        rowsInline.add(rowInline);
        inlineKeyboard.setKeyboard(rowsInline);
        return inlineKeyboard;
    }
}

And in our Constants file we add:

String TRAINING_TODAY = "Today";
String TRAINING_TOMORROW = "Tomorrow";
String FIND_TRAINING_DATE = "Do you want to have a workout today or tomorrow?";

Now we can just call the static method of our factory to make use of an inline keyboard. The most important code part is setCallbackData(). It defines an identifier to recognize which button has been clicked by the user. Hint: In a real world application it might not be too smart to use the button text as identifier for a callback but we use it here to simplify the code.

Now we need to use the keyboard in our response by adding another sender.execute() below the first one which we already defined.

public void replyToStart(long chatId) {
        try {
            sender.execute(new SendMessage()
                .setText(Constants.START_REPLY)
                .setChatId(chatId));

            sender.execute(new SendMessage()
                .setText(Constants.FIND_TRAINING_DATE)
                .setChatId(chatId)
                .setReplyMarkup(KeyboardFactory.withTodayTomorrowButtons()));

            chatStates.put(chatId, State.AWAITING_TRAINING_DAY);

        } catch (TelegramApiException e) {
                e.printStackTrace();
        }
}

Give it a try! Our bot will now offer an inline keyboard to answer the training day question.

Inline keyboard interactions

So far we aren’t able to recognize clicks on the two buttons. But we can give our FitnessBot a new ability to do so! Similar to the image reply example given by Telegram we can filter for button responses via Flag.CALLBACK_QUERY. All the identifiers of clicked buttons will be sent inside of the update object upd. This object contains a lot of data but luckily there are some helper methods to extract the important information conveniently. getChatId(upd) will find the chat id of the update for you and via AbilityUtils.getUser(upd) you can get the author of the message.

Add the following lines to your bot class.

public Reply replyToButtons() {
        Consumer<Update> action = upd -> responseHandler.replyToButtons(getChatId(upd), upd.getCallbackQuery().getData());
        return Reply.of(action, Flag.CALLBACK_QUERY);
}

The tricky part here is the implementation of replyToButtons() inside of responseHandler because every single button click will be processed in this method. That’s why we use it for separation of concerns only.

public void replyToButtons(long chatId, String buttonId) {
        try {
                switch (buttonId) {
                        case Constants.TRAINING_TODAY:
                                replyToTrainingToday(chatId);
                                break;
                        case Constants.TRAINING_TOMORROW:
                                replyToTrainingTomorrow(chatId);
                                break;
                }
        } catch (TelegramApiException e) {
                e.printStackTrace();
        }
}

The actual logic resides in the referenced methods. First, it validates whether the bot is currently in a state where it is waiting for a response. Only if this is the case, the button is clicked. Depending on which button has been pressed, the bot is moved to another state.

private void replyToTrainingToday(long chatId) throws TelegramApiException {
        if (chatStates.get(chatId).equals(State.AWAITING_TRAINING_DAY)) {
                sender.execute(new SendMessage()
                        .setText(Constants.TRAINING_TODAY_REPLY)
                        .setChatId(chatId));
                chatStates.put(chatId, State.TODAY_IS_TRAINING_DAY);
        }
}

private void replyToTrainingTomorrow(long chatId) throws TelegramApiException {
        if (chatStates.get(chatId).equals(State.AWAITING_TRAINING_DAY)) {
                sender.execute(new SendMessage()
                        .setText(Constants.TRAINING_TOMORROW_REPLY)
                        .setChatId(chatId));
                chatStates.put(chatId, State.TODAY_IS_RELAX_DAY);
        }
}

The two referenced states are new thus have to be added to our enum:

public enum State {
    AWAITING_TRAINING_DAY, TODAY_IS_TRAINING_DAY, TODAY_IS_RELAX_DAY
}

Depending on the clicked button we want to answer something different. As usual, we need to add the answers to Constants:

String TRAINING_TODAY_REPLY = "Okay then take this as a reminder ;)";
String TRAINING_TOMORROW_REPLY = "Okay I'll remind you tomorrow at nine o'clock!";

Extra: Scheduled task execution

Now we need a way to remind our users to do sports on their training days. For that, we make use of ScheduledExecutorService from Java’s concurrent package. We’re not going to go into detail here, as it is not part of the bot framework. You just need to know that with the class below we have an easy way to execute a task at a specific time on a daily basis. E.g. to write a message at 9am in the morning. Create a new class called DailyTaskExecutor:

public class DailyTaskExecutor {
    private final ScheduledExecutorService executorService;
    private final DailyTask dailyTask;

    public DailyTaskExecutor(DailyTask dailyTask) {
        this.executorService = Executors.newScheduledThreadPool(1);
        this.dailyTask = dailyTask;
    }

    public void startExecutionAt(int targetHour, int targetMin, int targetSec) {
        Runnable taskWrapper = () -> {
            dailyTask.execute();
            startExecutionAt(targetHour, targetMin, targetSec);
        };
        long delay = computeNextDelay(targetHour, targetMin, targetSec);
        executorService.schedule(taskWrapper, delay, TimeUnit.SECONDS);
    }

    private long computeNextDelay(int targetHour, int targetMin, int targetSec) {
        LocalDateTime localNow = LocalDateTime.now();
        ZoneId currentZone = ZoneId.systemDefault();
        ZonedDateTime zonedNow = ZonedDateTime.of(localNow, currentZone);
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec);
        if(zonedNow.compareTo(zonedNextTarget) >= 0)
            zonedNextTarget = zonedNextTarget.plusDays(1);

        Duration duration = Duration.between(zonedNow, zonedNextTarget);
        return duration.getSeconds();
    }

    public void stop() {
        executorService.shutdown();
        try {
            executorService.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException ex) {
            Logger.getLogger(DailyTaskExecutor.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

DailyTask is an interface with an execute() method. We have to create an implementation of this interface later.

public interface DailyTask {
    void execute();
}

But first, let’s create an instance of DailyTaskExecutor in our FitnessBot class as a new field. As soon as we have our instance we schedule a new task which will run every morning at 9 am using startExecutionAt().

    private final DailyTaskExecutor dailyTaskExecutor;

    private FitnessBot(String botToken, String botUsername) {
        super(botToken, botUsername);
        responseHandler = new ResponseHandler(sender, db);
        dailyTaskExecutor = new DailyTaskExecutor(new MorningReminderTask(this));
        dailyTaskExecutor.startExecutionAt(9, 0, 0);
    }

Okay, now we need a new class for our DailyTask interface. We call it MorningReminderTask and it takes a callback listener as a constructor parameter. This callback listener is part of the class itself and gets called as soon as execute() will be run.

public class MorningReminderTask implements DailyTask {

    public interface Callback {
        void onTimeForMorningTask();
    }

    private final Callback callback;

    public MorningReminderTask(Callback callback) {
        this.callback = callback;
    }

    @Override
    public void execute() {
        callback.onTimeForMorningTask();
    }
}

Now you should have a compile error in your bot class because you try to pass your bot as a reference for callback in the constructor of MorningReminderTask. Before you can do that you need to implement MorningReminderTask.Callback in your bot.

public class FitnessBot extends AbilityBot implements MorningReminderTask.Callback {

    @Override
    public void onTimeForMorningTask() {
        responseHandler.sayMorningMessages();
    }
}

Due to that interface, you need to override onTimeForMorningTask() method. As our response handler class is taking care of writing messages we just call one of its methods. Which we have to create now:

public void sayMorningMessages() {
        try {
            for (long chatId : chatStates.keySet()) {
                switch (chatStates.get(chatId)) {
                    case TODAY_IS_TRAINING_DAY:
                        processTrainingDay(chatId);
                        break;
                    case TODAY_IS_RELAX_DAY:
                        processRelaxDay(chatId);
                        break;
                }
            }
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }

    private void processTrainingDay(long chatId) throws TelegramApiException {
        sender.execute(new SendMessage()
                        .setText(Constants.TRAINING_REMINDER)
                        .setChatId(chatId));
        chatStates.put(chatId, State.TODAY_IS_RELAX_DAY);
    }

    private void processRelaxDay(long chatId) {
        chatStates.put(chatId, State.TODAY_IS_TRAINING_DAY);
    }

In the constants file:

String TRAINING_REMINDER = "Good Morning! Don't forget to do sports, today.";

For each chat, we will now send a reminder message if we are in TODAY_IS_TRAINING_DAY state. Furthermore, we are changing the state to the opposite, so every second day will be a training day.

And that’s it. Now your bot is able to remind you every second day to do sports. Feel free to leave a comment if you have any questions or suggestions. You can find the whole code of this getting started on our github profile.

Greets,
Domi

Leave a Reply

Your email address will not be published. Required fields are marked *