TelegramBot in the Wolfram Cloud

    Introduction


    The period passed when every second article on Habrahabr was devoted to writing a telegram bot. Also, a period of time passed when the bot could easily be placed on your computer or hosting in Russia. Just six months ago, my bot ran just on a laptop and did not experience any problems with connecting to the API. But now, when I thought about getting him back to work, I realized that it would not be so easy. I did not want to search and configure a proxy server, and even more so abroad. Also, before that, I wrote a bot on Wolfram Language and had no idea how the language works with proxy servers, since I have not used them yet. And then there was a great idea! Use the Wolfram Cloud. In this article I want to show how simple it is with registration, but without SMS you can run your simple telegram bot, written in Wolfram Language. From the tools you need for this only the browser.


    A bit about the Wolfram cloud


    To access the cloud you need to create a Wolfram account. To do this, go to https://account.wolfram.com and follow the instructions after clicking the Create One button.



    After all the manipulations done on the cloud's main page at https://www.wolframcloud.com all products and their usage plans will be displayed. You must select the Development Platform and create a new notepad.



    All the code given below will be executed in this cloud notebook.


    Just a little bit about telegram bots


    There are many articles devoted to them. Here you just have to say that before you perform all further actions, you need to create a bot in a standard way. That is, just start a chat with the @BotFather bot and send it the command:


    /newbot

    Then you just need to follow the instructions and enter the name and login. Let his name be Wolfram Cloud Bot and login @ WolframCloud5973827Bot.



    API implementation


    We will use the @BotFather recommendations and briefly examine the HTTP API of telegram bots. Tasks for the implementation of the entire API is not yet worth it. For writing a bot, only a small part is enough. Check that the API is available and the bot with the above token exists. To do this, just run one line:


    URLExecute["https://api.telegram.org/bot753681357:AAFqdRFN_QoODJxsBy3VN2sVwWTPKJEqteY/getMe"]

    Out [..]: = ...
    {"ok" -> True, 
     "result" -> {"id" -> 753681357, "is_bot" -> True, 
       "first_name" -> "Wolfram Cloud Bot", 
       "username" -> "WolframCloud5973827Bot"}}

    The command above is the easiest way to perform an HTTP request from Wolfram Language. But let's make it a bit more complicated so that all other API methods can be easily implemented. Create a common method for executing an API request:


    TelegramBot::usage = "TelegramBot[token]";
    $telegramAPI = "https://api.telegram.org";
    telegramExecute[
        TelegramBot[token_String], method_String, 
        parameters: {(_String -> _)...}: {}
    ] := Module[{
        request, requestURL, requestRules, requestBody, 
        response, responseBody
    }, 
        requestURL = URLBuild[{$telegramAPI, "bot" <> token, method}];
        requestRules = DeleteCases[parameters, _[_String, Automatic | Null | None]];
        requestBody = ImportString[ExportString[requestRules, "JSON"], "Text"];
        request = HTTPRequest[requestURL, <|
            Method -> "POST", 
            "ContentType" -> "application/json; charset=utf-8", 
            "Body" -> requestBody
        |>];
        response = URLRead[request];
        responseBody = response["Body"];
        Return[ImportString[responseBody, "RawJSON"]]
    ]

    Check if this works on the method already tested above:


    token = "753681357:AAFqdRFN_QoODJxsBy3VN2sVwWTPKJEqteY";
    bot = TelegramBot[token];
    telegramExecute[bot, "getMe"]

    Out [..]: = ...
    <|"ok" -> True, 
     "result" -> <|"id" -> 753681357, "is_bot" -> True, 
       "first_name" -> "Wolfram Cloud Bot", 
       "username" -> "WolframCloud5973827Bot"|>|>

    Fine. Let's create a separate function to perform a bot check:



    getMe::usage="getMe[bot]";
    TelegramBot /: 
    getMe[bot_TelegramBot] := 
    telegramExecute[bot, "getMe"]
    getMe[bot]

    Out [..]: = ...
    <|"ok" -> True, 
     "result" -> <|"id" -> 753681357, "is_bot" -> True, 
       "first_name" -> "Wolfram Cloud Bot", 
       "username" -> "WolframCloud5973827Bot"|>|>

    Now it remains to add the basic methods that are needed to create a bot in the cloud:


    • getUpdates - gets all the latest posts written to bot

    getUpdates::usage = "getUpdates[bot, opts]";
    Options[getUpdates] = {
        "offset" -> Automatic,
        "limit" -> Automatic, 
        "timeout" -> Automatic, 
        "allowed_updates" -> Automatic
    };
    TelegramBot /: 
    getUpdates[bot_TelegramBot, opts: OptionsPattern[getUpdates]] := 
    telegramExecute[bot, "getUpdates", Flatten[{opts}]]

    • setWebhook - sets the server address for handling updates

    setWebhook::usage = "setWebhook[bot, url, opts]";
    Options[setWebhook] = {
        "certificate" -> Automatic, 
        "max_connections" -> Automatic, 
        "allowed_updates" -> Automatic
    };
    TelegramBot /: 
    setWebhook[bot_TelegramBot, url_String, opts: OptionsPattern[setWebhook]] := 
    telegramExecute[bot, "setWebhook", Join[{"url" -> url}, Flatten[{opts}]]]


    deleteWebhook::usage = "deleteWebhook[bot]";
    TelegramBot /: 
    deleteWebhook[bot_TelegramBot] := 
    telegramExecute[bot, "deleteWebhook"]


    getWebhookInfo::usage = "getWebhookInfo[bot]";
    TelegramBot /: 
    getWebhookInfo[bot_TelegramBot] := 
    telegramExecute[bot, "getWebhookInfo"]


    sendMessage::usage = "sendMessage[bot, chat, text]";
    Options[sendMessage] = {
        "parse_mode" -> Automatic, 
        "disable_web_page_preview" -> Automatic, 
        "disable_notification" -> Automatic, 
        "reply_to_message_id" -> Automatic, 
        "reply_markup" -> Automatic
    };
    TelegramBot /: 
    sendMessage[bot_TelegramBot, chat_Integer, text_String, 
        opts: OptionsPattern[sendMessage]] := 
    telegramExecute[
        bot, "sendMessage", 
        Join[{"chat_id" -> chat, "text" -> text}, Flatten[{opts}]]
    ]

    Minimal API version is ready. Check how sending a message and receiving updates work. To do this, create a chat with our bot. When creating a bot, the first message with the / start text will be sent. Let's see whether it got into the list of updates:


    updates = getUpdates[bot]

    Out [..]: = ...
    <|"ok" -> True, 
     "result" -> {<|"update_id" -> 570790461, 
        "message" -> <|"message_id" -> 1, 
          "from" -> <|"id" -> 490138492, "is_bot" -> False, 
            "first_name" -> "Kirill", "last_name" -> "Belov", 
            "username" -> "KirillBelovTest"|>, 
          "chat" -> <|"id" -> 490138492, "first_name" -> "Kirill", 
            "last_name" -> "Belov", "username" -> "KirillBelovTest", 
            "type" -> "private"|>, "date" -> 1542182547, 
          "text" -> "/start", 
          "entities" -> {<|"offset" -> 0, "length" -> 6, 
             "type" -> "bot_command"|>}|>|>}|>

    You can get the latest update data from the list of updates:


    lastUpdate = updates["result"][[-1]]

    Out [..]: = ...
    <|"update_id" -> 570790461, 
     "message" -> <|"message_id" -> 1, 
       "from" -> <|"id" -> 490138492, "is_bot" -> False, 
         "first_name" -> "Kirill", "last_name" -> "Belov", 
         "username" -> "KirillBelovTest"|>, 
       "chat" -> <|"id" -> 490138492, "first_name" -> "Kirill", 
         "last_name" -> "Belov", "username" -> "KirillBelovTest", 
         "type" -> "private"|>, "date" -> 1542182547, "text" -> "/start", 
       "entities" -> {<|"offset" -> 0, "length" -> 6, 
          "type" -> "bot_command"|>}|>|>

    And this is how you can get the chat from which the message came and the message text itself:


    chat = lastUpdate["message", "chat", "id"]
    text = lastUpdate["message", "text"]

    Out [..]: = ...
    490138492
    /start

    As can be seen from the result of the execution - everything is in place. Now we will send a message on behalf of the bot using sendMessage.


    sendMessage[bot, chat, "hello"]

    Out [..]: = ...
    <|"ok" -> True, 
     "result" -> <|"message_id" -> 2, 
       "from" -> <|"id" -> 753681357, "is_bot" -> True, 
         "first_name" -> "Wolfram Cloud Bot", 
         "username" -> "WolframCloud5973827Bot"|>, 
       "chat" -> <|"id" -> 490138492, "first_name" -> "Kirill", 
         "last_name" -> "Belov", "username" -> "KirillBelovTest", 
         "type" -> "private"|>, "date" -> 1542182601, "text" -> "hello"|>|
     >


    In general, this set of functions is already enough. However, using the getUpdates method is not very convenient. You need to think of a way to handle messages using a webhook.


    Create webhook


    In Wolram Langauge there is a special kind of functions that are created using the APIFunction. Here is an example of one of these:


    apiFunc = APIFunction[{"n" -> "Integer"}, Plot[Sin[#n * x], {x, -2Pi, 2Pi}]&, "PNG"];
    apiFunc[{"n"->3}]

    Out [..]: = ...


    These features are specifically designed for deployment in the cloud. This function will accept one request parameter as input. To deploy it in the cloud, it is enough to transfer the function itself to CloudDeploy.


    apiObject = CloudDeploy[apiFunc, "Deploy/apiObject"]

    Out [..]: = ...
    CloudObject[https://www.wolframcloud.com/objects/kirillbelovtest/apiObject]

    Then you can follow the received link in the browser and add a query parameter:



    The function above handled the request parameters. So you need to create the same function to process the HTTP request body, received from the telegram bot in the form of an Update object. To generate an address, we use a token to access the cloud object was more difficult. It is also necessary to indicate that the object has public access, otherwise the telegrams will not be able to get on the webhook.


    deployWebhook[bot_TelegramBot, handler_] := 
        CloudDeploy[APIFunction[{}, handler[HTTPRequestData["Body"]] &],
            "Deploy/Webhooks/" <> Hash[bot, "SHA", "HexString"], 
            Permissions -> "Public"
        ]

    handler is another handler function. Let the handler turn the request body string into an association, get the chat identifier from there and send back the word "hello".


    handlerHello[bot_TelegramBot][body_String] := 
        Block[{json = ImportString[body, "RawJSON"], chat},         chat = json["message", "chat", "id"];        sendMessage[bot, chat, "hello"];    ]

    Now let's deploy the function in the cloud.


    webhookObject = deployWebhook[bot, handlerHello[bot]]

    Out [..]: = ...
    CloudObject[https://www.wolframcloud.com/objects/kirillbelovtest/Deploy/Webhooks/b9bd74f89348faecd6b683ba02637dd4d4028a28]

    And the last step - give the address of this object telegram-bot.


    setWebhook[bot, webhookObject[[1]]]

    Out [..]: = ...
    <|"ok" -> True, "result" -> True, "description" -> "Webhook was set"|>

    Now we’ll write something to bot and see what it says:



    The dialogue can be considered valid. In order to change the logic of an existing handler, it is enough to redeploy the cloud object. At the same time, installing the webhook for the bot is no longer required.


    Response logic


    This will be the last part in the process of creating a bot in the Wolfram cloud. Further in the same way, you can complicate the logic and add new API methods. Now about the dialogue itself. Let, after sending the command / start, the bot returns the answer "Hello" and changes the user's keyboard. The keyboard remains only two buttons: "Hello" and "Who are you?". We realize the dialogue in the form of an association. The keys are the commands that the user sends to the bot. Values ​​of keys - the answer of the bot and the new keyboard. At the same time, the set of keys and buttons must completely coincide. Otherwise, a situation may appear when the bot does not know what to answer. In such cases, of course, you can add a default response.


    keyboard[buttons : {__String}] := 
     {"keyboard" -> {Table[{"text" -> button}, {button, buttons}]}, 
      "resize_keyboard" -> True}
    $answers = <|
        (*user_text-><|"answer"->bot_text,"keyboard"->next_text|>*)
        "/start"-><|"answer"->"Привет","keyboard"->
            keyboard[{"Привет","Кто ты?"}]|>, 
        "Привет"-><|"answer"->"Как дела?",
            "keyboard" -> keyboard[{"А твои?"}]|> , 
        "А твои?"-><|"answer"->"Нормально",
            "keyboard" -> keyboard[{"Назад"}]|> , 
        "Кто ты?"-><|"answer"->"Бот написанный на Wolfram Language специально для статьи", 
            "keyboard"->keyboard[{"Какая статья?","Кто автор?"}]|> , 
        "Какая статья?"-><|"answer"->"вот ссылка на нее:\nhttps://habr.com/post/422517/", 
            "keyboard"->keyboard[{"Назад","Кто автор?"}]|> , 
        "Кто автор?"-><|"answer"->"Вот этот пользователь:\n@KirillBelovTest", 
             "keyboard"->keyboard[{"Какая статья?","Назад"}]|> ,  
        "Назад"-><|"answer"->"Привет", 
            "keyboard"->keyboard[{"Привет","Кто ты?"}]|>
    |>;
    answer[text_String] /; KeyExistsQ[$answers, text] := $answers[text]

    Now create a handler:


    handlerAbout[bot_TelegramBot][body_String] := 
        Block[{json = ImportString[body, "RawJSON"], chat, text}, 
            chat = json["message", "chat", "id"];
            text = json["message", "text"];
            sendMessage[bot, chat, answer[text]["answer"], 
                "reply_markup" -> answer[text]["keyboard"]];
        ]

    And redeploy the cloud object:


    deployWebhook[bot, handlerAbout[bot]];

    Gain that happened in the chat with the bot. But first, let's clear the message history:



    Expansion of functionality


    So far, there are no fundamental differences from the huge number of existing bots. Maybe there is no point in his drinking either? The meaning of all the work done above will be, if you understand the actual benefits of such a bot! After all, he can use all the features of Wolfram Language and Wolrfam Cloud. It is necessary that the robot was able to solve equations? It is very easy! It is only necessary to determine the answer!


    answer[text_String]["keyboard"] /; 
      StringContainsQ[text, " найти "] := Automatic 
    answer[text_String]["answer"] /; StringContainsQ[text, " найти "] := 
        ToString[Flatten[Block[{args = StringSplit[text, " найти "]},         Solve[ToExpression[args[[1]]], ToExpression[args[[2]]]]    ]]]
    deployWebhook[bot, handlerAbout[bot]];


    If someone additionally has an interest in the capabilities of the cloud, then a good description of its functionality is here .


    Restrictions


    Wolfram Cloud is a platform that allows you to use the Wolfram language for free, while the main product of Wolfram Research - Mathematica costs money. Accordingly, there are restrictions on the use and in my opinion they are very strong. When using the free Development Platform, the user is given 1,000 cloud credits per month. Each cloud loan gives time to calculate a different type. Since the article talks about CloudDeploy + APIFunction, such objects stored in the cloud spend 1 credit in 0.1 seconds of computing time. It is easy to calculate that the user is given only 1 minute for free and 40 seconds of server time for the operation of his application (in this case, the bot). I have nothing to add here - it is very, very small. The main focus on users who work in the Development Platform independently using a browser. Indeed, in this mode there are no time limits, but only on the duration of the session and the resources allocated. With this use, the Development Platform is almost a complete Mathematica, but does not require installation and a license.


    Article in the Wolfram Cloud


    Also popular now: