Back to Home

Launch the Telegram bot on an Android device (Remote Bot for Telegram)

android · telegram bots · telegram api

Launch the Telegram bot on an Android device (Remote Bot for Telegram)

Four months ago, I had the idea to write a Telegram bot that would run not on an external server, like most bots, but on a mobile phone.

The idea was not born from scratch: I often missed incoming calls and SMS when the phone was in my jacket or pocket, so I needed an additional notification method. And since I actively use Telegram on my computer, I thought that it would not be bad if incoming SMS and missed calls came to Telegram. After a little digging, I decided to write a bot.

Prototype development


I began to study the topic of creating Telegram bots using official documentation and examples. Basically, all the examples were written in Python. Therefore, without hesitation, I began to look for ways to run the Python server on Android. But after evaluating the time to learn Python and not finding anything suitable for starting the server, I started looking for alternatives and came across several Java libraries to write Telegram bots. As a result, he stopped on a project from Pengrad: java-telegram-bot-api .

This library allowed, at that time, to initialize the bot and receive-send messages that I needed and needed. Having added the library to my project, I implemented a simple service that launched a loop in the background thread to receive messages from Telegram and process them. Previously, it was necessary to register a new bot through the parent bot @Botfather and get its token. Read more about creating a bot here .

In order for the service not to be killed by the system when the device is off the screen, when the service starts, WakeLock was installed.

I’ll give an example of a function that allows you to receive the latest messages and send them for processing:

private void getUpdates (final TelegramBot bot)
private void getUpdates(final TelegramBot bot) {
        try {
            GetUpdatesResponse response = bot.execute(
                    new GetUpdates()
                            .limit(LIMIT)
                            .offset(updateId.get())
                            .timeout(LONG_POLLING_TIMEOUT));
            if (response != null && response.updates() != null && response.updates().size() > 0) {
                for (Update update : response.updates()) {
                    obtainUpdate(bot, update);
                    updateId.set(update.updateId() + 1);
                }
            }
        } catch (Exception e) {
            ErrorUtils.log(TAG, e);
        }
    }


Later, for security reasons, I added the ability to bind the bot to allowed Telegram accounts and the ability to prohibit the execution of certain commands for given users.

Having added several commands for the bot, such as sending, reading SMS, viewing missed calls, battery information, determining location, etc., I published the application on Google Play, created topics on several forums, and began to wait for comments and feedback.

Mostly the reviews were good, but the problem of high battery consumption was revealed, which, as you might have guessed, was due to WakeLock and the constant activity of the service.

This helped a little, but another problem appeared, AlarmManager did not work correctly on some Chinese devices. And therefore, the bot sometimes did not wake up after several hours spent in a state of sleep. Studying the official documentation, I read that Long Polling is not the only way to receive messages, messages could still be received using Webhook.

Receiving Messages via Webhook


I signed up for Digital Ocean , created a VPS on Ubuntu, then implemented the simplest Java http server using the Spark Framework . You can make 2 types of requests to the server: push (sending push notifications via webhook) and ping.

Push notifications were sent using Google Firebase.

Example class to help push notifications
public class PushHelper {
    private static final String URL = "https://fcm.googleapis.com/fcm/send";
    private static java.util.logging.Logger log = java.util.logging.Logger.getLogger(PushHelper.class.getName());
    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    private static final String AUTHORIZATION = "...";
    public static String push(PushRequest pushRequest) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        return post(URL, objectMapper.writeValueAsString(pushRequest));
    }
    private static String post(String url, String json) throws IOException {
        RequestBody body = RequestBody.create(JSON, json);
        Request request = new Request.Builder()
                .url(url)
                .header("Authorization", AUTHORIZATION)
                .post(body)
                .build();
        OkHttpClient client = getSslClient();
        if (client != null) {
            Response response = client.newCall(request).execute();
            return response.body().string();
        } else {
            throw new IOException("Unable to init okhttp client");
        }
    }
...
}


The request model for sending push notifications
public class PushRequest {
    private PushData data; //Данные, отправляемые на устройство
    private String to;  //Пуш-токен устройства
    private String priority = "high"; //Приоритет сообщения
    ...
}

In order for the message to arrive even when the device is in a sleep state, you must specify priority = "high"

SSL certificate generation


Having tested sending push notifications, I began to figure out how to configure and start the server with HTTPS, as this is one of the requirements when receiving messages from Telegram via webhook.

A free certificate can be generated using the letsencrypt.org service , but one of the limitations is that the specified host cannot be an ip address when generating the certificate. I did not want to register a domain name yet, especially since the official documentation of the Telegram Bot API allows the use of self-signed certificates, so I began to figure out how to create my certificate.

After several hours spent in attempts and searches, we got a script that allows us to generate the necessary certificate.

create_cert.sh
openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 365 -out public_cert.pem -subj "/C=RU/ST=State/L=Location/O=Organization/CN=ServerHost"
openssl pkcs12 -export -in public_cert.pem -inkey private.key -certfile public_cert.pem -out keystore.p12
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -sigalg SHA1withRSA -destkeystore keystore.jks -deststoretype JKS
rm keystore.p12
rm private.key


After running the script, the output is two files: keystore.jks - used on the server, public_cert.pem - used when installing webhook in an Android application.

In order to start HTTPS on the Spark Framework, it is enough to add 2 lines, one indicating the port (allowed ports for webhook: 443, 80, 88, 8443), the other indicating the generated certificate and password for it:

port(8443);
secure("keystore.jks", "password", null, null);

To install webhook for the bot, you need to add the following lines to the android application:

SetWebhook setWebHook = new SetWebhook().url(WEBHOOK_URL + "/" + pushToken + "/" + secret).certificate(getCert(context));
BaseResponse res = bot.execute(setWebHook);

When registering a webhook, the webhook address is specified as the URL, then the push token necessary for sending push notifications and the secret key generated on the device, which I added to additionally check incoming notifications, are sent.

The function of reading a public certificate from a RAW resource:

private static byte[] getCert(Context context) throws IOException {
        return IOUtils.toByteArray(context.getResources().openRawResource(R.raw.public_cert));
}

After modifying the message processing service in the Android application, the bot began to consume a lot less battery, but the dependence of the application on the push notification server was added, which was necessary for the application to work stably.

Automatically create a bot


After updating the mechanism for receiving messages, another problem remained that did not allow the application to use a certain percentage of users due to the complexity of creating a bot through BotFather. So I decided to automate this process.

The tdlib library from the creators of Telegram helped me with this . Unfortunately, I found very few examples of using this library, but having understood the API, it turned out that not so complicated. As a result, it was possible to implement authorization in Telegram by phone number, adding @Botfather to the contact list and sending and receiving messages to the specified contact, and in the specific case, to @Botfather bot.

Example functions for sending and receiving messages
private Observable sendMessage(long chatId, String text) {
        return Observable.create(subscriber -> {
            telegramClient.sendMessage(chatId, text, object -> {
                if (object instanceof TdApi.Error) {
                    subscriber.onError(new Throwable(((TdApi.Error) object).message));
                } else {
                    TdApi.Message message = (TdApi.Message) object;
                    subscriber.onNext(message);
                }
            });
        }).delay(5, TimeUnit.SECONDS).flatMap(msg -> getLastIncomingMessage(((TdApi.Message) msg).chatId, ((TdApi.Message) msg).senderUserId, ((TdApi.Message) msg).id));
    }
    private Observable getLastIncomingMessage(long chatId, int userId, int outgoingMessageId) {
        return Observable.create(subscriber -> {
            telegramClient.getLastIncomingMessage(chatId, outgoingMessageId, userId, object -> {
                if (object instanceof TdApi.Error) {
                    subscriber.onError(new Throwable(((TdApi.Error) object).message));
                } else {
                    TdApi.Message message = (TdApi.Message) object;
                    subscriber.onNext(message);
                }
            });
        });
    }


TelegramClient.java - wrapper class over TdApi

public class TelegramClient {
    private final Client client;
    public TelegramClient(Context context, Client.ResultHandler updatesHandler) {
        TG.setDir(context.getCacheDir().getAbsolutePath());
        TG.setFilesDir(context.getFilesDir().getAbsolutePath());
        client = TG.getClientInstance();
        TG.setUpdatesHandler(updatesHandler);
    }
    public void clearAuth(Client.ResultHandler resultHandler) {
        TdApi.ResetAuth request = new TdApi.ResetAuth(true);
        client.send(request, resultHandler);
    }
    public void getAuthState(Client.ResultHandler resultHandler) {
        TdApi.GetAuthState req = new TdApi.GetAuthState();
        client.send(req, resultHandler);
    }
    public void sendPhone(String phone, Client.ResultHandler resultHandler) {
        TdApi.SetAuthPhoneNumber smsSender = new TdApi.SetAuthPhoneNumber(phone, false, true);
        client.send(smsSender, resultHandler);
    }
    public void checkCode(String code, String firstName, String lastName, Client.ResultHandler resultHandler) {
        TdApi.CheckAuthCode request = new TdApi.CheckAuthCode(code, firstName, lastName);
        client.send(request, resultHandler);
    }
    public void sendMessage(long chatId, String text, Client.ResultHandler resultHandler) {
        TdApi.InputMessageContent msg = new TdApi.InputMessageText(text, false, false, null, null);
        TdApi.SendMessage request = new TdApi.SendMessage(chatId, 0, false, false, null, msg);
        client.send(request, resultHandler);
    }
    public void getLastIncomingMessage(long chatId, int fromMessageId, int userId, Client.ResultHandler resultHandler) {
        getChat(chatId, chatObj -> {
            if (chatObj instanceof TdApi.Chat) {
                TdApi.GetChatHistory getChatHistory = new TdApi.GetChatHistory(chatId, fromMessageId, -1, 2);
                client.send(getChatHistory, messagesObj -> {
                    if (messagesObj instanceof TdApi.Messages) {
                        TdApi.Messages messages = (TdApi.Messages) messagesObj;
                        if (messages.totalCount > 0) {
                            for (TdApi.Message message : messages.messages) {
                                if (message.id != fromMessageId && message.senderUserId != userId) {
                                    resultHandler.onResult(message);
                                    return;
                                }
                            }
                        }
                        resultHandler.onResult(new TdApi.Error(0, "Unable to get incoming message"));
                    } else resultHandler.onResult(messagesObj);
                });
            } else resultHandler.onResult(chatObj);
        });
    }
    public void getChat(long chatId, Client.ResultHandler resultHandler) {
        TdApi.GetChat getChat = new TdApi.GetChat(chatId);
        client.send(getChat, resultHandler);
    }
    public void searchContact(String username, Client.ResultHandler resultHandler) {
        TdApi.SearchPublicChat searchContacts = new TdApi.SearchPublicChat(username);
        client.send(searchContacts, resultHandler);
    }
    public void getMe(Client.ResultHandler resultHandler) {
        client.send(new TdApi.GetMe(), resultHandler);
    }
    public void changeUsername(String username, Client.ResultHandler resultHandler) {
        client.send(new TdApi.ChangeUsername(username), resultHandler);
    }
    public void startChatWithBot(int botUserId, long chatId, Client.ResultHandler resultHandler) {
        TdApi.CloseChat closeChat = new TdApi.CloseChat(chatId);
        client.send(closeChat, resClose -> {
            TdApi.OpenChat openChat = new TdApi.OpenChat(chatId);
            client.send(openChat, resOpen -> {
                if (resOpen instanceof TdApi.Error) {
                    resultHandler.onResult(resOpen);
                    return;
                }
                TdApi.SendBotStartMessage request = new TdApi.SendBotStartMessage(botUserId, chatId, "/start");
                client.send(request, resultHandler);
            });
        });
    }
    public void logout(Client.ResultHandler resultHandler) {
        client.send(new TdApi.ResetAuth(false), resultHandler);
    }
}


Adding New Features


After solving the primary problems with autonomy, I started adding new teams.
As a result, such commands were added as: photo, video recording, voice recorder, screenshot, player control, launch of selected applications, etc. For convenient launch of commands, added a Telegram-keyboard and divided the commands into categories.

At the request of users, I also added the ability to call Tasker commands and send messages from Tasker to Telegram.

After that, I thought that it would be nice to add external access from third-party applications for sending messages to Telegram. Messages can be both text and include audio, video, location by coordinates. As a result, I wrote a library that you can add to your project.

Library
Usage example

Conclusion


In this article I tried to share a brief history of working on a project to create a bot running on an Android device and the difficulties that I encountered. Now I am doing the project in my free time, adding new teams and fixing errors that arise.

Thank you very much for your attention. I will be glad to hear from you useful comments and suggestions.

Links:
Application on Google Play
Channel in Telegram
Project website

Read Next