TelegramBot в облаке Wolfram

    Введение


    Прошел тот период, когда каждая вторая статья на Habrahabr была посвящена написанию своего телеграмм-бота. Также прошел период времени, когда бота без трудностей можно было разместить на своем компьютере или хостинге в России. Еще полгода назад мой бот запускался просто на ноутбуке и не испытывал никаких проблем с подключением к API. Но сейчас, когда я задумался над тем, чтобы вернуть его в работу, я понял, что это будет не так легко. Не хотелось искать и настраивать прокси-сервер и тем более за рубежом. Также до этого я писал бота на Wolfram Language и не имел представления о том, как язык работает с прокси-серверами, так как до сих пор их не использовал. И тут появилась замечательная идея! Использовать Wolfram Cloud. В этой статье я хочу показать, как очень просто с регистрацией, но без смс можно запустить своего простого телеграм-бота, написанного на Wolfram Language. Из инструментов понадобится для этого только браузер.


    Немного про облако Wolfram


    Чтобы получить доступ к облаку необходимо создать аккаунт Wolfram. Для этого нужно перейти по адресу https://account.wolfram.com и следовать инструкциям после нажатия на кнопку Create One.



    После всех проделанных манипуляций на главной странице облака по адресу https://www.wolframcloud.com будут отображаться все продукты и их планы использования. Необходимо выбрать Development Platform и создать новый блокнот.



    Весь код приведенный в дальнейшем, будет выполняться именно в этом облачном блокноте.


    Совсем немного о телеграмм-ботах


    Существует огромное множество статей посвященных им. Здесь всего лишь надо сказать, что перед тем как выполнять все дальнейшие действия, бота надо создать стандартным способом. То есть просто начать чат с ботом @BotFather и отправить ему команду:


    /newbot

    Дальше просто необходимо следовать инструкциям и ввести имя и логин. Пусть его имя будет Wolfram Cloud Bot и логин @WolframCloud5973827Bot.



    Реализация API


    Воспользуемся рекомендациями @BotFather и бегло осмотрим HTTP API телеграм-ботов. Задачи по реализации всего API целиком пока не стоит. Для написания бота достаточно только небольшой части. Проверим, что API доступно и бот с указанным выше токеном существует. Для этого достаточно выполнить всего одну строчку:


    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"}}

    Команда выше — самый простой способ выполнить HTTP запрос из Wolfram Language. Но немного усложним его, чтобы было легко реализовать все остальные методы API. Создадим общий метод выполнения запроса к API:


    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"]]
    ]

    Проверим работает ли это на уже протестированном выше методе:


    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"|>|>

    Отлично. Создадим отдельно функцию для выполнения проверки бота:


    • getMe — информация о боте

    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"|>|>

    Теперь подобным образом осталось добавить основные методы, которые необходимы для создания бота в облаке:


    • getUpdates — получает все последние сообщения написанные боту

    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 — устанавлиевает адрес серевера для обработки обновлений

    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 — отправка сообщения в чат

    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}]]
    ]

    Минимальная версия API готова. Проверим как работает отправка сообщение и получение обновлений. Для этого создадим чат с нашим ботом. При создании боту отправится первое сообщение с тектом /start. Посморим — попало ли оно в список обновлений:


    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"|>}|>|>}|>

    Получить из списка обновлений данные последнего обновления можно так:


    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"|>}|>|>

    А вот так можно получить чат, из которого пришло сообщение и сам текст сообщения:


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

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

    Как видно из результат выполнения — все на месте. Теперь отправим сообщение от имени бота используя 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"|>|
     >


    В общем-то этого набора функций уже достаточно. Однако, использовать метод getUpdates не очень удобно. Нужно придумать способ, как обрабатывать сообщения с помощью webhook.


    Создание webhook


    В Wolram Langauge есть специальный вид функций, которые создаются с помощью APIFunction. Вот пример одной из таких:


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

    Out[..] := ...


    Такие функции предназначены специально для развертывания в облаке. Данная функция будет принимать на вход один параметр запроса. Чтобы развернуть ее в облаке достаточно передать саму функцию в CloudDeploy.


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

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

    Затем можно перейти по полученной ссылке в браузере и добавить параметр запроса:



    Функция выше обрабатывала параменты запроса. Значит нужно создать такую же функцию для обработки тела HTTP запроса, прирходящего от телеграм-бота в виде объекта Update. Для генерации адреса используем токен, чтобы получить доступ к облачному объекту было сложнее. Также необходимо указать, что объект имеет публичный доступ, иначе телеграм не сможет попасть на webhook.


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

    handler — другая функция обработчик. Пусть обработчик превращает строку тела запроса в ассоциацию, получает оттуда идентификатор чата и высылает обратно слово "hello".


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

    Теперь развернем фунцкию в облаке.


    webhookObject = deployWebhook[bot, handlerHello[bot]]

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

    И последний шаг — передадим адрес этого объекта телеграм-боту.


    setWebhook[bot, webhookObject[[1]]]

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

    Теперь напишем что-нибуд боту и посмотрим что он ответит:



    Диалог можно считать состоявшимся. Для того чтобы изменить логику работы уже существующего обработчика — достаточно повторно выполнить развертывание облачного объекта. При этом выполнять установку webhook для бота уже не потребуется.


    Логика ответов


    Это будет последняя часть в процессе создания бота в облаке Wolfram. Дальше таким же образом можно усложнять логику и добавлять новые методы API. Теперь о самом диалоге. Пусть, после отправки команды /start бот возвращает ответ "Привет" и меняет клавиатуру пользователя. В клавиатуре остается всего две кнопки: "Привет" и "Кто ты?". Реализуем диалог в виде ассоциации. Ключами будут команды, которые высылает пользователь боту. Значения ключей — сам ответ бота и новая клавиатура. При этом множество ключей и кнопок должны полностью совпадать. Иначе может появиться ситуациция, когда бот не знает что ответить. В таких случаях, конечно, можно добавить ответ по умолчанию.


    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]

    Теперь создадим обработчик:


    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"]];
        ]

    И выполним повторно развертывание облачного объекта:


    deployWebhook[bot, handlerAbout[bot]];

    Приверим, что получилось в чате с ботом. Но для начала очистим историю сообщений:



    Расширение функциональности


    Пока что принципиальных отличий от огромного множества уже существующих ботов нет. Может и смысла в его напиании тоже нет? Смысл всей проделанной выше работы будет, если понять в чем собственно преимущества такого бота! Ведь он может использовать все возможности Wolfram Language и Wolrfam Cloud. Необходимо, чтобы робот умел решать уравнения? Это очень легко! Надо всего-лишь доопределить ответ!


    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]];


    Если у кого-то дополнительно появится интерес к возможностям облака — то хорошее описание его функциональности есть здесь.


    Ограничения


    Wolfram Cloud — платформа, которая позволяет использовать язык Wolfram бесплатно, в то время как основной продукт компании Wolfram Research — Mathematica стоит денег. Соответственно на использование есть ограничения и на мой взгяд они очень сильные. При использовании бесплатной версии Development Platform пользователю в месяц выдается 1000 облачных кредитов. Каждый облачный кредит дает время на вычисление различного типа. Так как в статье говоиртся про CloudDeploy + APIFunction — то такие объекты, хранящиеся в облаке тратят 1 кредит за 0.1 секунды вычислительного времени. Несложно подсчитать, что пользователю бесплатно выдается всего 1 минута и 40 секунд серверного времени на работу своего приложения (в данном случае бота). Мне здесь нечего добавить — это очень и очень мало. Основной упор на пользователей, которые работают в Development Platform самостоятельно с помощью браузера. Ведь в таком режиме нет никаких ограничений по времени, а только по длительности сессии и выделяемым ресурсам. При таком использовании Development Platform — это почти полноценная Mathematica, но не требующая установки и лицензии.


    Статья и код в Wolfram Cloud

    Wolfram Research
    77,54
    Wolfram Language, Mathematica, Wolfram Alpha и др.
    Поделиться публикацией

    Комментарии 6

      +1
      Интересно, спасибо.
      Напомнило, пару лет назад писал телеграмм бота — интерпретатора Octave (совместимый с Matlab язык)
        +1
        Превратить этого бота в интерпретатор Mathematica еще проще. Просто заменить:
        answer[text_String]["answer"] := ToString[ToExpression[text]]
        И все команды, которые должны возвращать текстовый результат будут работать. Отдельно нужно реализовать команды, которые возвращают график через sendPhoto. Тоже самое для аудио-файла или видео/анимации. Я хотел обратить особое внимание на возможность развертывания в облаке — тогда не требуется запускать отдельный процесс или докер-контейнер на компьютере или сервере.
        0
        Буквально вчера перенесли бота на AWS Lambda + AWS Api gateway. Если к боту менее 1 млн. обращений — ничего стоить не будет.
          0

          А менее 1 млн в месяц, или какой период?

            0
            1,000,000 free requests per month for AWS Lambda — в месяц. Можете посмотреть для большей информации внизу странички — AWS Уровень бесплатного пользования AWS (предложения без ограничения срока действия)
            Можно писать бот полностью на платформе AWS, но надо следить за лимитами, чтобы не влезть в долги в случае выхода за рамки бесплатных предложений. Можно писать бот на своем сервере/площадке/vds/vps, а AWS Lambda + AWS Api gateway использовать только для пересылки в api телеgрамmа (очень дешево). Если Вы новый клиент — 12 месяцев много чего бесплатно на попробовать предоставляется. По истечении 12 месяцев:
            • AWS Lambda — Первый миллион запросов в месяц – бесплатно потом 0,20 USD ЗА 1 МЛН дальнейших запросов
            • Amazon API Gateway — Первый миллион запросов в месяц – бесплатно потом 3,50 USD за 333 миллиона запросов
            • AWS Key Management Service — мы используем, но тоже не дорого
            • AmazonCloudWatch — мы используем для логов на этапе отладки, но тоже не дорого
          0
          Спасибо!

          ps Для мелочи сейчас использую heroku, очень удобно если не нужно 24/7 бота держать.

          pps Это для krb, веткой промахнулся.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое