📜 ⬆️ ⬇️

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

Four months ago, I had an idea to write a Telegram bot, which will be launched 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 a jacket or pocket, so I needed an additional method of notification. And since I actively use Telegram on a computer, I thought that it would not be bad if incoming SMS and missed calls came to Telegram. Having rummaged a little, I decided to write a bot.

Prototype development


I began to study the subject 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 launch the Python server on Android. But after evaluating the time to learn Python and not finding anything suitable for running the server, I started searching for alternatives and came across several Java libraries to write Telegram bots. As a result, I stopped at a project from Pengrad: java-telegram-bot-api .

This library allowed, at that time, to initialize the bot and receive-send messages, which was what I needed. Having added the library to my project, I implemented a simple service that started a cycle in the background thread for receiving messages from Telegram and processing them. Previously, it was necessary to register a new bot through the parent bot @Botfather and get its token. Learn more about creating a bot using the link .
')
In order for the service not to be killed by the system, when the device is off the screen, when starting the service, WakeLock was installed.

I will cite as an example 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 link a bot to authorized Telegram accounts and the ability to prohibit the execution of certain commands for specified 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 threads on several forums, waited for comments and feedback.

The reviews were mostly good, but the problem of high battery consumption was revealed, which, as you might have guessed, was connected with WakeLock and constant service activity. After a bit of hard work, I decided to periodically start the service via the AlarmManager, then after receiving the messages and responding to them, stop the service.

This helped a little, but another problem appeared, AlarmManager did not work correctly on some Chinese devices. And so 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.

Receive messages via Webhook


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

Push notifications were sent using Google Firebase.

An example of a class that helps send 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"); } } ... } 


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 the sleep state, you need to specify priority = "high"

SSL certificate generation


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

A free certificate can be generated using the letsencrypt.org service, but one of the limitations is that the indicated host cannot be an ip address when generating a certificate. I did not want to register a domain name yet, especially 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 the attempts and searches, it turned out a script that allows you to generate the desired 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, we get two files at the output: keystore.jks - used on the server, public_cert.pem - used when installing the webhook in the Android application.

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

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

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

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

When registering a webhook, the URL is the webhook address, then the push token that is required to send push notifications and the secret key generated on the device, which I added for additional checking of incoming notifications, is transmitted.

Function of reading public certificate from 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 use the battery much less, but it also added the dependence of the application on the push notification server, which was a necessity for the stable operation of the application.

Automatic bot creation


After updating the mechanism for receiving messages, another problem remained that did not allow a certain percentage of users to use the application due to the difficulty 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 the use of this library, but having understood the API, it turned out that everything is not so difficult. As a result, we managed to implement authorization in Telegram by phone number, adding @Botfather to the contact list and sending and receiving messages to a given contact, and in a particular case, to bot @Botfather.

Example of functions for sending and receiving messages
 private Observable<TdApi.Message> 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<TdApi.Message> 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 - class wrapper 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 commands.
As a result, such commands were added as: photo, video recording, voice recorder, screenshot of the screen, player control, launch of selected applications, etc. For a convenient command launch, I 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 to send messages to Telegram. Messages can be both text and include audio, video, location by coordinates. In the end, I wrote a library that you can add to your project.

→ Library
→ Example of use

Conclusion


In this article, I tried to share a brief history of work on a project to create a bot working on an Android device and the difficulties I encountered. Now I am working on a project in my spare time, adding new commands and correcting errors that occur.

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

References:
→ Application in Google Play
→ Channel in Telegram
→ Project site

Source: https://habr.com/ru/post/318882/


All Articles