Русский
Русский
English
Статистика
Реклама

Telegram api

Из песочницы Urban Bot или как писать чат-ботов для Telegram, Slack, Facebook на React.js

27.07.2020 22:10:16 | Автор: admin

image


В этой статье я хочу познакомить с новой библиотекой Urban Bot, которая адаптирует React для написания чат-ботов. Ниже я расскажу, зачем эта библиотека появилась на свет, какие дает преимущества и как написать вашего первого чат-бота.


Чат-бот это чаще всего отдельный чат в мессенджере, в котором вы общаетесь не с человеком, а с программой. Он может присылать сообщения в виде текста, изображений, кнопок и многих других UI элементов и реагировать на сообщения от пользователей. Современные чат-боты это полноценные UI приложения внутри мессенджеров.

В отличии от большинства чат-бот библиотек, которые чаще всего просто оборачивают http запросы в функции с готовыми аргументами и предоставляют подписки вида bot.on('message', callback), иногда позволяя передавать контекст между вызовами, Urban Bot предлагает совершенно иной подход к разработке чат-ботов через декларативное программирование и компонентный подход. Живой пример, написанный на Urban Bot, вы можете попробовать в Telegram, cсылка на чат-бот, и посмотреть код на GitHub.


Как мы заметили выше, чат-боты это полноценные UI приложения. А какой язык в 2020 и какая библиотека наиболее подходит для разработки UI приложений? Правильно, JavaScript и React. Такая интеграция позволяет легко и непринужденно строить чат-боты любой сложности без единого знания об API мессенджеров. Далее я расскажу, как создавать простые компоненты и на их основе строить сложных чат-ботов, работать с навигацией, создавать диалоги любой вложенности, писать одно приложение и запускать в любых мессенджерах, и многое другое.


Отправка сообщений


Так выглядит самый простой пример на Urban Bot. Для отправки текстового сообщения нам нужно создать функцию и вернуть из него готовый компонент Text с текстом внутри, который мы хотим отправить. Когда компонент отрендериться, все пользователи чат-бота получат сообщение "Hello, world!".


import React from 'react';import { Text } from '@urban-bot/core';function App() {    return (        <Text>           Hello, world!        </Text>    );}

Изображение можно отправить так:


import React from 'react';import { Image } from '@urban-bot/core';function App() {    const imageByURL =  'https://some-link.com/image.jpg';    return <Image file={imageByURL} />;}

Urban Bot имеет готовый набор компонентов, для каждого вида сообщений, для файлов File, для кнопок ButtonGroup и много других, подробнее можно взглянуть здесь. В каждый из них можно передать определенный набор пропсов, например, имитировать будто бот печатает сообщение 1 секунду <Text simulateTyping={1000}>.


Получение сообщений


Мы рассмотрели как посылать сообщения, давайте разберемся как подписываться на сообщения от пользователей. За подписки в Urban Bot отвечают React Hooks.


Чтобы подписаться на текстовые сообщения, мы можем использовать хук useText.


import React from 'react';import { Text, useText } from '@urban-bot/core';function App() {    useText(({ text }) => {        console.log(`Пользователь отправил сообщение ${text}`);    });    return (        <Text>            Hello, world!        </Text>    );}

Urban Bot предоставляет готовы набор хуков для разных типов сообщений. Например, useImage, если пользователь отправил изображение, useFile и т.д. Полный список здесь. В каждый хук также приходит мета информация, кто отправил сообщение и т.д.


Эхо бот


Теперь, зная базовые концепции, мы можем соединить получение и отправку сообщения. Напишем простой компонент, который после получения сообщения отправит его пользователю обратно.


В этом компоненте мы впервые добавим работу с переменными через React хук useState. Этот хук возвращает переменную и функцию, чтобы ее изменять. React.useState нужен, чтобы изменение переменной приводило к ререндеру и, соответсвенно, отправке нового сообщения. Мы определим начальное значение переменной text как "Привет" и передадим в компонент Text. Также мы подпишемся на сообщения от пользователей с помощью хука useText, и будем изменять text через функцию setText. После вызова setText React перерендерит компонент Echo с новым значением, и пользователь получит новое сообщение с тем что он сам отправил боту.


import React from 'react';import { Text, useText } from '@urban-bot/core';function Echo() {    const [text, setText] = React.useState('Привет!');    useText(({ text }) => {        setText(text);    });    return (        <Text>            {text}        </Text>    );}

Кнопки


Давайте также напишем пример с кнопками, сделаем простейший счетчик. Для этого нам понадобятся компоненты ButtonGroup и Button. Каждой кнопке мы определим свой обработчик, который будет менять count на +1 или -1 и будем передавать результат в ButtonGroup в проп title. Мы установим проп isNewMessageEveryRender как false, чтобы при последующих ререндерах отправлялось не новое сообщение с новыми кнопками, а просто изменялось начальное сообщение.


import React from 'react';import { ButtonGroup, Button } from '@urban-bot/core';function Counter() {    const [count, setCount] = React.useState(0);    const increment = () => setCount(count + 1);    const decrement = () => setCount(count - 1);    return (        <ButtonGroup title={count} isNewMessageEveryRender={false}>            <Button onClick={increment}>+1</Button>            <Button onClick={decrement}>-1</Button>        </ButtonGroup>    );}


Мы написали два простых компонента эхо и счетчик. Давайте представим, что мы хотим чтобы они были у нас в одном чат-боте, но работали независимо. Для этого мы можем подключить компонент Router и разделить компоненты по разным путям. Пути это обычные сообщения от пользователей.


Теперь, когда пользователь напишет "/echo" отрендериться компонент Echo, когда "/counter" управление перейдет в Counter. Роуты также могут принимать path как regexp.


import React from 'react';import { Router, Route } from '@urban-bot/core';import { Echo } from './Echo';import { Counter } from './Counter';function App() {    return (        <Router>            <Route path="/echo">                <Echo />            </Route>            <Route path="/counter">                <Counter />            </Route>        </Router>    );}

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


image


Если вы почувствовали концепцию, то уже поняли, что можно создавать десятки, сотни компонентов, комбинировать их вместе, либо разделять роутером, делиться компонентами между разными чат-ботами и строить действительно сложные SPA внутри мессенджеров.


Форматирование текста


Urban Bot позволяет стилизовать сообщения через привычные HTML теги. Писать жирным <b>, курсивом <i>, зачеркнутым <s>, переносить строки <br /> и так далее, полный список здесь.


Пример
const someCode = `function sum2() {    return 2 + 2;}if (sum2() !== 4) {    console.log('WTF');}`;<Text>    Usual text    <br />    <b>Bold text</b>    <br />    <i>Italic text</i>    <br />    <u>Underscore text</u>    <br />    <s>Strikethrough text</s>    <br />    <q>quote</q>    <br />    <b>        Bold and <s>Strikethrough text</s>    </b>    <br />    <code >Code 2 + 2</code >    <br />    <pre>{someCode}</pre>    <br />    <a href="http://personeltest.ru/aways/github.com/urban-bot/urban-bot">External link</a></Text>


Диалоги


Чат-боты могут вести диалоги с пользователем, что-то спрашивать, реагировать на сообщения в зависимости от ответа пользователя. В Urban Bot встроен компонент Dialog, который позволяет легко создавать диалоги любой сложности.


Пример плоского диалога. Бот задаст первый вопрос, после ответа задаст второй, потом третий, потом запустит callback onFinish с готовыми ответами. На каждом шаге можно обрабатывать ответ пользователя через callback onNext.


import React from 'react';import { Dialog, DialogStep, Text } from '@urban-bot/core';function FlatDialogExample() {    return (        <Dialog onFinish={(answers) => console.log(answers)}>            <DialogStep                content={<Text>Привет, как тебя зовут?</Text>}                 id="name"                onNext={(name) => console.log(name)}            >                <DialogStep                    content={<Text>Cколько тебе лет?</Text>}                    id="age"                >                    <DialogStep                         content={<Text>Из какого ты города?</Text>}                        id="city"                    />                </DialogStep>            </DialogStep>        </Dialog>    );}

Можно получать на следующем шаге прошлый ответ через паттерн render-props .


function FlatDialogExample() {    return (        <Dialog>            <DialogStep content={<Text>Привет, как тебя зовут?</Text>}>                {(name) => (                    <DialogStep                         content={<Text>{`${name}, cколько тебе лет?`}</Text>}                    />                )}            </DialogStep>        </Dialog>    );}

Можно добавить валидацию на каждый шаг.


function FlatDialogExample() {    return (        <Dialog onFinish={(answers) => console.log(answers)}>            <DialogStep                content={<Text>Привет, как тебя зовут?</Text>}                id="name"                validation={{                     isValid: (answer) => answer !== 'Самуэль',                     errorText: 'Самуэль заблокирован.'                 }}            >                // ...            </DialogStep>        </Dialog>    );}

Также возможно построение более сложных диалогов, когда бот отвечает определенным образом в зависимости от ответа пользователя. Для предопределенных ответов можно использовать кнопки.


import React from 'react';import { Dialog, DialogStep, Text, ButtonGroup, Button } from '@urban-bot/core';function TreeDialogExample() {    return (        <Dialog>            <DialogStep                content={                    <ButtonGroup title="Привет, что вы хотите купить?">                        <Button id="hat">Футболка</Button>                        <Button id="glasses">Очки</Button>                    </ButtonGroup>                }            >                <DialogStep                    match="hat"                    content={                        <ButtonGroup title="Футболка какого размера?">                            <Button id="m">S</Button>                            <Button id="s">M</Button>                            <Button id="l">L</Button>                        </ButtonGroup>                    }                />                <DialogStep                    match="glasses"                    content={                        <ButtonGroup title="Очки какого цвета?">                            <Button id="black">Черный</Button>                            <Button id="white">Белый</Button>                        </ButtonGroup>                    }                />            </DialogStep>        </Dialog>    );}

Состояние


Что вы можете использовать для управления состоянием? Все то же что и в любом React приложении. Можете использовать React useState и передавать состояние ниже по дереву компонентов через пропсы или React Context. Можно использовать библиотеки для управления состоянием: Redux (пример), MobX (пример), Apollo и любые другие, которые обычно используют вместе с React, вы можете даже переиспользовать готовые части из готовых React Web или React Native приложений, так как Urban Bot использует тот же чистый React, который работает в миллионах приложений.


Сессия


Urban Bot запускается на сервере, соответсвенно, все логика от всех чатов и пользователей обслуживается на одном железе, что кардинально отличается от стандартных React приложений, которые запускаются на стороне каждого клиента. Urban Bot решает эту проблему на своей стороне и отображает приложение уникальным для каждого пользователя, так что, если вы уже писали на React веб или мобильные приложения, вы не почувствуете разницу.


В любом примере на Urban Bot уже вшита работа с сессией. Например, в примере ниже у каждого пользователя, кто зайдет в персональный чат-бот, будет его собственный счетчик.


function Counter() {    const [count, setCount] = React.useState(0);    const increment = () => setCount(count + 1);    const decrement = () => setCount(count - 1);    return (        <ButtonGroup title={count} isNewMessageEveryRender={false}>            <Button onClick={increment}>+1</Button>            <Button onClick={decrement}>-1</Button>        </ButtonGroup>    );}

Если нам понадобится глобальный счетчик, то к этому примеру можно сделать хук, который будет содержать уже общую информацию.


function Counter() {    const [count, setCount] = useGlobalCount();   // ...}

Также, в любом месте приложения мы можем получить данные о конкретном чате, какой пользователь написал, с каким никнеймом и т.д. Если чат-бот персональный, то chat и from будут совпадать.


import React from 'react';import { Text, useText, useBotContext } from '@urban-bot/core';function UserId() {    const { chat } = useBotContext();    useText(({ from }) => console.log(`Пришло сообщение от ${from.username}`));   return <Text>Чат id {chat.id}</Text>;}

Типизация


Urban Bot написан на TypeScript, соответсвенно проект полностью типизирован, и если вы пишете на TypeScript, вам будет очень удобно.


Запуск в мессенджерах


Большой плюс Urban Bot, что он не привязан ни к одному мессенджеру. Есть основной пакет @urban-bot/core, который позволяет создавать абстрактных чат-ботов, а уже их подключать к определенным мессенджерам. В данный момент есть поддержка Telegram, Slack, Facebook. В дальнейшем, мы планируем добавлять любые мессенджеры, где есть чат-боты и открытое API. Если вам интересно, и вы хотите писать Urban Bot приложения для других мессенджеров, скажем Viber, Discord или у вас есть свой мессенджер то пишите к нам в группу https://t.me/urbanbotjs, одной просьбы будет достаточно, чтобы появилось большая мотивация реализовать ваш функционал.


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


Скажем, у нас есть готовое приложение App и мы хотим его запустить его в Telegram. Для этого нам понадобится класс UrbanBotTelegram из пакет @urban-bot/telegram. Функция render из @urban-bot/core подобная ReactDOM.render и компонент Root. Мы создаем экземпляр UrbanBotTelegram и передаем туда бот токен из Telegram, также можно передать isPolling, чтобы не настраивать вебхук, и бот работал локально. Готовый экземпляр мы передаем в компонент Root, и оборачиваем наше готовое приложение и, соответсвенно, передаем все в функцию render, которая запустит все процессы.


Более подробная инструкция с видео как разрабатывать локально и деплоить Telegram бота можно прочитать в инструкции.


import React from 'react';import { render, Root } from '@urban-bot/core';import { UrbanBotTelegram } from '@urban-bot/telegram';import { App } from './App';const urbanBotTelegram = new UrbanBotTelegram({    token: 'telegramToken',    isPolling: true,});render(    <Root bot={urbanBotTelegram}>        <App />    </Root>);

Если нам нужно запустить два и более мессенджера, просто используем пакеты других мессенджеров, передаем туда нужные данные и делаем по аналогии с Telegram.


// ...import { UrbanBotSlack } from '@urban-bot/slack';// ...render(    <Root bot={urbanBotTelegram}>        <App />    </Root>);const urbanBotSlack = new UrbanBotSlack({    signingSecret: 'slackSigningSecret',    token: 'slackToken',});render(    <Root bot={urbanBotSlack}>        <App />    </Root>);

Прямой вызов API


С помощью Urban Bot вы можете создавать чат-ботов просто описывая их через компоненты. А что если вам будет нужно вручную вызвать API? Каждый экземпляр UrbanBot* содержит в себе API клиент для активного мессенджера. Рассмотрим пример для Telegram.


Мы можем получить bot с помощью хука useBotContext. bot содержит client и type c типом мессенджера. client будет представлять собой экземпляр библиотеки node-telegram-bot-api . В любом месте приложения можно получить client и вызвать любой метод на ваше усмотрение, скажем блокировать пользователя, если он написал нецензурное сообщение.


import React from 'react';import { useText, useBotContext } from '@urban-bot/core';function SomeComponent() {    const { bot } = useBotContext();    useText(({ text, chat, from }) => {        if (text.includes('бл***')) {            bot.client.kickChatMember(chat.id, from.id);        }    });    // ...}

В каждом мессенджере уникальный API. Если вы разрабатываете несколько мессенджеров одновременно, можно отделять функционал сравнивая bot.type.


import { useBotContext } from '@urban-bot/core';import { UrbanBotTelegram } from '@urban-bot/telegram';import { UrbanBotSlack } from '@urban-bot/slack';function SomeComponent() {    const { bot } = useBotContext();    if (bot.type === UrbanBotTelegram.type) {        // telegram api        bot.client.kickChatMember(/* ... */);    }    if (bot.type === UrbanBotSlack.type) {        // slack api        bot.client.conversations.kick(/* ... */);    }    // ...}

Как попробовать?


У Urban Bot есть стартер, который позволит вам начать разрабатывать чат-ботов за минуту, сделан по аналогии с create-rect-app. Все что вам нужно, чтобы попробовать Urban Bot это выполнить команду в терминале для


TypeScript


npx create-urban-bot my-app

JavaScript


npx create-urban-bot my-app --template js

Команда склонирует шаблон приложения с нужными зависимостями и скриптами на ваш компьютер, и вам останется только открыть проект, передать ваши токены в .env файл и расскоментировать мессенджеры, которые вы хотите разрабатывать. Видео инструкция.


Итого


Несмотря на то что Urban Bot запустился только недавно, на мой взгляд это библиотека с огромным потенциалом. Только представьте, если у вас есть базовые знания React, вы можете написать чат-бот любой сложности на все возможные платформы, создавать и использовать библиотеки с готовым набором компонентов на манер ui-kit, переиспользовать код между вашими другими UI приложеними на React, будь то web или mobile.


Если вы уже разрабатывали чат-ботов, попробуйте Urban Bot, вы почувствуете как библиотека делает за вас кучу работы. Если никогда не разрабатывали, но имеете представление о React, то напишете вашего первого чат-бота за 5 минут. Если вам понравилась идея и хотите, чтобы проект развивался дальше, вступайте в нашу группу в Telegram, ставьте звезду на гитхабе, будем рады любому фидбеку.


Полезные ссылки


Сайт
Github
Группа в Telegram
Наглядный чат-бот в Telegram, с открытым кодом.
Как создать Todo List чат-бот в Telegram с помощью React.js

Подробнее..

Botsman новая платформа для разработки Telegram-ботов

30.12.2020 12:20:04 | Автор: admin

Сегодня, в предпоследний день уходящего года, хочу рассказать о созданном мной сервисе, помогающем быстро проектировать, отлаживать и следить за работоспособностью ботов в мессенджере Телеграм. Надеюсь, он окажется удобным инструментом. Под катом довольно подробный рассказ о том, как этот сервис зародился, какие технологии я для него выбрал и обзор того, что он сейчас умеет.

Для тех же, кому уже захотелось ознакомиться с Botsman (но не очень хочется много читать) вот ссылка, милости прошу: https://bots.mn/. Главное, о чём стоит помнить платформа только-только запустилась, и (пока что) не стоит переносить на неё что-то серьёзное и масштабное.

Предыстория: путь к созданию Ботсмана

Пять лет назад в Telegram появилась возможность создавать ботов автоматизированные аккаунты, которыми можно управлять с помощью своих скриптов. Меня это сразу заинтересовало: я люблю делать маленькие, но полезные утилитки.

Правда, в отличие от десктопных приложений, браузерных расширений и простых веб-сервисов, с ботами есть некоторая сложность в их разработке: в отличие от традиционных веб-сервисов, их труднее отлаживать. Даже если настроить логирование, бывает трудно понять, что именно происходит с ботом и всё ли работает так, как нужно.

Поэтому почти сразу пришла в голову идея сделать такую платформу, которая возьмёт весь мониторинг на себя: будет логировать и входящие обновления, и то, как бот на них реагирует (и какие возникают при этом ошибки). Заодно можно и графики удобные строить.

Но, к сожалению, дальше придумывания красивого названия Botsman и покупки короткого домена bots.mn дело поначалу не зашло. Проект оказался непривычно большим для того, чтобы осилить его в одиночку, и за пять лет я то брался за него, то снова откладывал на потом.

И только к концу злополучного 2020-го я нашёл в себе силы довести его до рабочего состояния, которое можно показать широкой публике. Специально поставил цель зарелизиться до Нового года: чтобы добавить этому году плюс одно хорошее событие :)

Итак, что же сейчас предлагает данная платформа?

Проксирование запросов

Во-первых, мой сервис умеет работать как прокси: вы подключаете бота к Botsman, он ловит все приходящие от Telegram события, логирует, и передаёт вашему серверу. Это удобно, если у вас уже написан и где-то крутится бот: ваш код может даже не подозревать про эту прослойку.

Выбор способа обработки запросов ботомВыбор способа обработки запросов ботом

У этой фичи есть очевидный недостаток: небольшое увеличение времени отклика (поскольку в цепочке Telegram ваш сервер появляется дополнительное звено).

Но этой осенью Telegram сделал крутую вещь: они выложили в открытый доступ код сервера-посредника Bot API. По своей сути это такое приложение, которое внутри общается с Телеграмом как клиент по их протоколу MTProto, а снаружи у него торчит уже простое и понятное Bot API. И когда вы обращаетесь к публичному Bot API по HTTPS запрос на самом деле идёт к инстансу такого сервера, а теперь стало можно поднять его самому. И конечно же, внутри Ботсмана я так и сделал (и это новшество оказалось ещё одним мотиватором закончить проект).

Таким образом, вашего бота можно настроить так, что цепочка не станет длиннее: вместо
Telegram сервер Bot API ваш сервер будет
Telegram Botsman ваш сервер.

Правда, тут уже потребуются правки в коде вашего бота: исходящие запросы придётся делать не на api.telegram.org, а на api.bots.mn/telegram. Зато Botsman сможет логировать и их тоже!

Собственно, поговорим о логировании:

Живая лента обновлений

После настройки бота в Botsman, можно сразу открыть страницу Events, отправить что-то в Телеграме своему боту, и увидеть, как это сообщение появилось на экране в реальном времени. Если у вас настроен прокси вы увидите и результат перенаправления запроса вашему серверу. Если ваш сервер шлёт запросы через проксирующий эндпоинт api.bots.mn/telegram они тоже туда попадут.

Так в интерфейсе Botsman выглядит лог всех происходящих с ботом событийТак в интерфейсе Botsman выглядит лог всех происходящих с ботом событий

С технической стороны, конечно, тут всё просто: я использую веб-сокеты чтобы проталкивать все обновления на клиент, если у вас открыта соответствующая вкладка (если открытых страниц нет, лишней нагрузки это создавать не должно). Возможно, если бы я начинал этот проект сейчас, я бы попробовал использовать Server-sent events вместо веб-сокетов, но вряд ли есть какой-то повод переходить на них.

Кроме бесконечного лога, куда все события сыпятся вперемешку, Botsman ещё умеет показывать интерфейс, похожий на обычный список диалогов в самом мессенджере то, как их видит ваш бот. При желании можно даже никак не обрабатывать запросы, а просто вручную открывать диалоги и отвечать от имени бота (этакий режим техподдержки возможно, впоследствии я добавлю поддержку управления ботами с нескольких аккаунтов с разными ролями, если это действительно будет актуально).

Слежение за показателями бота

Ну и конечно же, статистика и графики, куда без них. Честно говоря, аналитик из меня так себе, поэтому сейчас Botsman показывает только довольно базовые метрики общее число апдейтов, число чатов, число пользователей, дневную и месячную аудиторию (DAU и MAU). Графики по числу апдейтов на каждый день/час, и по среднему времени обработки запросов. Было бы, конечно, интересно смотреть на всякую демографию, но в Telegram в этом плане мало информации о пользователях.

Графики в разделе Stats. Как видно, через одного из моих ботов уже прошло почти 20 млн апдейтов.Графики в разделе Stats. Как видно, через одного из моих ботов уже прошло почти 20 млн апдейтов.

Для подсчёта статистики я с самого начала выбрал старый добрый Redis, а вот на клиентской стороне решил рисовать графики с помощью библиотеки, которую до этого сам же написал на конкурс того же Телеграма. Не пропадать же добру :)

Скриптинг

Но вернёмся к тому, как вообще оживить вашего бота. Просто проксировать запросы к другому серверу скучно (да и от дополнительного звена всё-таки лучше избавиться). Мне хотелось дать возможность прямо внутри сервиса описать логику работы бота. Знаю, что существует множество конструкторов ботов для этого (хотя я и старался всё делать без оглядки на них), но мне хотелось возможности писать полноценный код прямо в браузере.

При этом мне не очень хотелось прибегать к компилируемым в нативный код языкам: это производительно, но тогда пришлось бы строить сложную систему, оборачивая код каждого бота в контейнер и держать их все непрерывно запущенными.

Поэтому я выбрал JavaScript: моя изначальная идея была взять встроенную в Node песочницу, немного доработать (как это сделано в библиотеках Sandcastle или vm2), чтобы сделать её безопаснее, и выполнять код ботов в ней.

Но в процессе очередного подхода к допиливанию проекта пришло осознание, что даже с доработками такой подход неидеален: код в таком случае выполнялся бы все равно в одном общем потоке и при росте нагрузке неизбежно привёл бы к тормозам. Да и риск проблем с изолированностью исполняемого кода всё-таки сохраняется.

В итоге я обратил внимание на библиотеку isolated-vm: она тоже реализует песочницу в JS, но делает это другим, более безопасным (и, что важно, многопоточным) образом. По сути это обёртка над присутствующим в V8 механизмом изолятов независимых контекстов, которые ничего не знают друг про друга. Эта же библиотека, кстати, используется в игре Screeps, где игрокам тоже нужно писать своих ботов.

Что особенно хорошо для ботов: состояние одного такого изолята можно держать в памяти как кусок данных (не расходуя процессорное время), и только когда боту понадобится обработать прилетевший апдейт от Телеграма оживлять это состояние, вызывая в нём функцию-обработчик. И конечно можно ограничить как потребляемую память, так и процессорное время (с этим, помнится, в нодовской песочнице были некоторые сложности).

Скриптинг: внутреннее API, обработчики событий

Дальше одним из камней преткновения было дать удобное внутреннее API для написания ботов. Конечно, можно было сказать вашему коду будет доступна переменная update и метод callMethod, а дальше делайте, что хотите. Но раз уж я проектирую всю песочницу, нужно идти до конца.

В частности, мне хотелось реализовать механизм состояний: часто боты рассчитаны на пошаговый диалог, и это надо учесть, как-то запоминать контекст, даже если пользователь бросил диалог и вернулся к нему на следующий день. В итоге я решил, что для каждого чата, пользователя или сообщения я позволю хранить объект с произвольными полями. А у чатов (как личных, так и групповых) в придачу есть понятие пути, route оно позволяет раскидать обработчики событий по тому, на каком шаге находится пользователь.

Само добавление обработчиков делается довольно просто:

on(ctx => {  ctx.log('Some update received: ', update);});

Это самый универсальный обработчик: он вызывается при любом событии (напомню, кроме события пришло сообщение Telegram присылает и ряд других оповещений).

Перед функцией-коллбэком можно указать один конкретный тип события: тогда обработчик будет вызываться исключительно для него.

on('message', ctx => {  ctx.message.reply('Hi!');});

А что будет, если объявить два обработчика и они оба подходят для текущего апдейта? Botsman вызовет только первый из них но можно передать управление следующему, если вернуть false (ну или промис, резолвящийся в false разумеется, всё делалось с расчётом на асинхронный код).

Ещё есть удобные способы обработывать только текстовые сообщения с помощью on.text (их можно заодно ещё и фильтровать по регэкспу), только команды с помощью on.command, инлайн-запросы on.inline, и коллбэк-запросы (нажатия на кнопки под отправленными сообщениями) on.callback. О них можно почитать в документации.

В примерах выше видно, что обработчику передаётся единственный параметр экземпляр класса EventContext, и через него делаются все вызовы. Это позволяет Ботсману связывать, например, вызовы API или возникающие ошибки с конкретным апдейтом, который к ним привёл.

Ну а как разграничить обработчики для разных состояний (путей) чата? Для этого предназначена глобальная функция state:

state('step1', (on, state) => {  // Этот обработчик вызовется для любого сообщения,  // если наш чат в состоянии 'step1' - и переведёт его  // в состояние 'step2'  on.text(ctx => {    ctx.route.to('step2');   });});state('step2', (on, state) => {  // А этот обработчик вызывается, если наш чат в  // состоянии 'step2' и возвращает его в 'step1'  on.text(ctx => {    ctx.route.to('step1');   });});

Обратите внимание: функция state немедленно вызывает переданный ей коллбэк с двумя аргументами, которые заменяют собой глобальные функции on и state. Добавленный с помощью локальной функции on обработчик будет вызываться только в указанном состоянии, а с помощью локальной функции state можно создавать вложенные состояния (хотя их можно создать и вручную, просто записывая путь, разделённый слэшами: 'step1/substep1/branchA'). Пока что, впрочем, иерархическая структура состояний особых преимуществ по сравнению с линейной не имеет (но может помочь их логически упорядочить).

Скриптинг: форматируем сообщения с помощью tagged template literals

Отдельно поделюсь одной, казалось бы, незначительной, но весьма радующей лично меня деталью. Если вы уже пробовали использовать Telegram API, то возможно сталкивались со сложностями при отправке текста с форматированием особенно когда в него нужно подставить пользовательские данные. Telegram умеет принимать и HTML, и Markdown, но и в том, и в другом случае подставляемые данные нужно обрабатывать, эскейпить управляющие символы, что не очень удобно.

К счастью, не так давно при отправке сообщения (и в других методах, где можно отправлять форматированный текст) появилась возможность просто указать отдельно, какие участки в нём нужно отформатировать. Добавляем к этому tagged templates из ES6 и получаем вот что:

await ctx.call('sendMessage', {  chat_id: 12345,  ...fmt`Hello ${fmt.bold(foo)}! You can combine ${fmt.italic(bar).bold()} styles together.Links are supported ${fmt.text_link(linkLabel, linkUrl)}.`,});

Выглядит немного непривычно, зато а) не нужно ничего эскейпить, б) невозможно сломать вёрстку, потеряв какой-нибудь HTML-тэг или символ разметки Markdown. Если у вас валидный JS будет и валидная вёрстка. Под капотом запись fmt`something` возвращает объект с двумя полями text и entities поэтому его нужно распаковывать с помощью ... (spread syntax). Ну или его можно передать напрямую в короткие методы типа ctx.message.reply(fmt`something`) или ctx.chat.say(fmt`something`).

Мне кажется, что у tagged template literals вообще не очень много уместных применений в реальном мире, но тут у меня получилось найти одно из них :)

Скриптинг: код по расписанию и запросы к внешним API

Должен сделать важную оговорку: так как код выполняется в изолированных контекстах, у скриптов нет ни доступа к API самой Node, ни возможности импортировать внешние модули. Однако я реализовал метод fetch (по аналогии с одноимённым браузерным API) он позволяет делать не слишком тяжёлые запросы к внешним серверам. Кроме того, доступна глобальная функция cron с помощью неё можно запланировать регулярное выполнение повторяющихся действий:

cron('0 0 * * FRI', ctx => {  ctx.log('This function should execute each Friday at midnight');});

Первый параметр тут либо привычный формат crontab, либо интервал в секундах (правда, и то, и другое с разрешением не чаще раза в пять минут). Я думаю, что возможности выполнять код по расписанию и делать запросы к внешним API позволят реализовать множество практических задач. И не исключено, что в будущем список возможностей (например, импорт каких-то проверенных модулей) будет расширяться.

Скриптинг: веб-интерфейс

Писать код прямо в браузере не самое комфортное занятие, но я стремлюсь сделать его хотя бы сравнимым с разработкой в IDE. Сейчас в качестве редактора я использую CodeMirror (в первую очередь из-за его относительно небольшого веса), в дальнейшем, вероятно, добавлю возможность переключиться на Monaco он ощутимо тяжелее, зато должен быть шустрее и функциональнее (именно он используется внутри VS Code).

Так сейчас выглядит редактор кода в BotsmanТак сейчас выглядит редактор кода в Botsman

Пока что в Botsman нет разбивки кода на отдельные файлы в этом тоже может быть неудобство, если вы собираетесь писать сложного бота. Зато код сохраняется автоматически, и есть возможность хранить все его предыдущие версии это ещё не Git, но всё же.

И ещё одна небольшая, но довольно ценная возможность: проверка кода перед деплоем с помощью тестов. При этом Botsman по нажатию одной кнопки сразу покажет подробный лог выполнения (см. скриншот выше): время инициализации, обработки запроса, отладочный вывод и возникшие ошибки (код с синтаксическими ошибками задеплоить не выйдет в принципе).

Песочница для запросов к Telegram

Ну и последняя небольшая, но довольно полезная фича Botsman, о которой хотелось бы рассказать панель для формирования запросов к Telegram. Нередко социальные сети предоставляют собственный инструмент для этого, но у Телеграма его, к сожалению, нет. Поэтому я сделал страницу, где можно выбрать нужный метод API, заполнить его параметры и наглядно увидеть результат вызова. Заодно можно сразу скопировать эквивалентный код, делающий этот запрос.

Панель вызова методов Telegram APIПанель вызова методов Telegram API

Будущие планы

Как было сказано ранее, Botsman находится в самом начале своего пути. Возможно, его даже настигнет Хабраэффект (надеюсь, что нет!). Возможно, ему станет тяжко, если созданные на нём боты наберут популярность не исключено, что тогда придётся вводить платные возможности. Поскольку занимаюсь им я сейчас в одиночку, сложно сказать, что с ним будет.

В очень примерных планах сейчас такие фичи:

  • Визуальный конструктор в дополнение к скриптингу

  • Глобальное key-value хранилище + создание собственных хранилищ

  • Поддержка других платформ, кроме Telegram

  • Доступный снаружи эндпоинт для вызова кода бота

  • Управление ботом с нескольких аккаунтов

  • Навигация по коду, разбивка на модули, поддержка сторонних модулей

  • Более гибкое тестирование кода, автоматические тесты

  • Больше статистики и графиков

  • Оповещения (если с ботом что-то не так)

  • Улучшение вида чатов

  • Улучшение работы с файлами (скачивание, загрузка), в том числе в песочнице

В любом случае, надеюсь, что из этого сервиса выйдет что-то хорошее! Буду рад услышать ваши мысли, пожелания и соображения в комментариях.

Подробнее..

Из песочницы Бот Telegram для пиццерии на Python с помощью telebot

23.06.2020 12:07:45 | Автор: admin

Вступление


На Хабре уже есть статья о Telegram боте, написанном на Python с помощью telebot. Признаться, свое знакомство с чат-ботами в недавно разблокированном мессенджере я начинал с этой статьи. Моя писанина это дополнение, включающее в себя работу с Inline кнопками и базой данных.


Установка библиотеки


Telebot библиотека для взаимодействия с Telegram API, которая привлекла меня простотой, поэтому я считаю, что для новичков она подходит на все 100%.


Чтобы установить библиотеку, введите в терминале следующую команду.


pip install pytelegrambotapi


Получение API-токена


Чтобы создать бота, обратимся к "отцу" всех ботов @BotFather. Отправьте команду /newbot, после чего введите имя бота, затем алиас, оканчивающийся на bot.


image

Пишем код


Лучше записывать API-токены, номера кошелька и тому подобное в отдельный файл, поэтому создадим файл config.py:


TOKEN = "1176585885:AAH-RA2kZym9E5tR8JFLtYYjNxrMHnsJr0o"

Основной код находится в bot.py


# импорт библиотекиimport telebot# Подтягиваем токен бота и все необходимое для получение средств за пиццуfrom config import TOKEN# Создание ботаbot = telebot.TeleBot(TOKEN)# Декоратор для обработки всех текстовых сообщений@bot.message_handler(content_types=['text'])def all_messages(msg):    # Получаем сообщение пользователя    message = msg.text    # Получаем Telegram id пользователя (очевидно, для каждого пользователя он свой)    user_id = msg.chat.id    # Отправляем сообщение    bot.send_message(user_id, f"Вы написали: {message}")# Запускаем бота, чтобы работал 24/7if __name__ == '__main__':    bot.polling(none_stop=True)

Мы создали эхо-бота. Подумаем, что будет в нашем боте:


  • База данных. В ней храним информацию по пользователям, а также создадим таблицу для хранения информации о пицце. Для взаимодействия с БД лучше написать отдельные классы для пользователя и пиццы.
  • Клавиатуры. Помогут сделать интерфейс удобным.
  • Оплата. Для этого будем использовать сервис QIWI.

База данных


Я пользуюсь SQLiteStudio для создания баз данных.


Структура таблицы для пользователя



  • id уникальный идентификатор пользователя. Таким выступает id пользователя в мессенджере, поэтому без зазрения совести будем использовать его.
  • stat статус работы с ботом. Запоминаем на каком шаге остановился пользователь.
  • ord. Здесь мы в виде строки храним заказ пользователя. Пока я не представляю как сделать по-другому, поэтому предлагайте идеи в комментариях.
  • random_code. В будущем будем генерировать рандомный код для оплаты через QIWI.
  • time. Пользователь выбирает, когда хочет получить заказ, а мы сохраняем в это поле.

Структура таблицы для пиццы



Данные по пиццам:



По name получаем описание пиццы и её стоимость.Осталось добавить фотографию. Создадим директорию data, поместим в нее img, а уже там будут храниться изображения.



Классы для взаимодействия с базой данных


Так, базу данных создали, теперь приступим к классам. Создадим файл user.py, в нём класс User. Сначала напишем пару функций для подключения к базе данных и закрытию соединения.


# Подключаем библиотекуimport sqlite3class User:    # Функция для соединения с БД    def connect_to_db(self):        self.connect = sqlite3.connect('PizzaApplication.db')        self.cursor = self.connect.cursor()    # Закрываем соединение с БД    def close(self):        self.connect.close()

Бота открыл пользователь, которого мы не знаем. Поэтому нужны функции добавления нового юзера и получения всех пользователей. Второе нужно для того, чтобы проверять наличие каждого написавшего "/start" в БД.


# Получаем id всех пользователей, позже будем проверять наличие пользователя в этом списке    def get_all_id(self):        self.connect_to_db()        request = "SELECT id FROM user"        result = self.cursor.execute(request).fetchall()        self.close()        return [i[0] for i in result]    # Добавляем нового пользователя    def add_id_to_db(self, user_id):        self.connect_to_db()        request = "INSERT INTO user(id, stat) VALUES(?, ?)"        self.cursor.execute(request, (user_id, "start"))        self.connect.commit()        self.close()

Для получения и обновления данных можно написать по две функции на каждое из полей, но я предлагаю поступить по-другому. Структура таких функций будет идентичная, за исключением названия столбца, поэтому в функцию будем передавать поле.


    # Получаем заданное поле по пользователю    def get_field(self, user_id, field):        self.connect_to_db()        request = f"SELECT {field} FROM user WHERE id=?"        result = self.cursor.execute(request, (user_id,)).fetchone()        self.close()        return result[0]    # Меняем значение поля    def set_field(self, user_id, field, value):        self.connect_to_db()        request = f"UPDATE user SET {field}=? WHERE id=?"        self.cursor.execute(request, (value, user_id))        self.connect.commit()        self.close()

Основные функции для пользователя написаны. Позже будем возвращаться к этому классу, а пока приступим к пицце. Создадим файл pizza.py, в нём класс Pizza. Начальные функции такие же, как у User.


import sqlite3class Pizza:    # Подключение к базе данных    def connect_to_db(self):        self.connect = sqlite3.connect('PizzaApplication.db')        self.cursor = self.connect.cursor()    # Закрытие базы данных    def close(self):        self.connect.close()

Нужно получать данные из БД.


    def get_field(self, pizza_name, field):        self.connect_to_db()        request = f"SELECT {field} FROM pizza WHERE name=?"        result = self.cursor.execute(request, (pizza_name,)).fetchone()        self.close()        return result[0]

Для админ-панели потребуется функция set_field (подобная есть в классе User), но её я пока не предусмотрел.


В следующей статье разберём клавиатуры и начнём писать код в bot.py. Спасибо за внимание!

Подробнее..

Telegram-бот на Java для самых маленьких от старта до бесплатного размещения на heroku

25.11.2020 10:14:05 | Автор: admin


Для кого написано


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

Предыстория


Когда моя дочь начала изучать арифметику, я между делом накидал алгоритм генерации простых примеров на сложение и вычитание вида 5 + 7 =, чтобы не придумывать и не гуглить для неё задания.

И тут на глаза попалась новость, что Telegram выпустил новую версию Bot API 5.0. Ботов я раньше не писал, и потому решил попробовать поднять бота как интерфейс для своей поделки. Все примеры, которые мне удалось найти, показались либо совсем простыми (нужные мне функции не были представлены), либо очень сложными для новичка. Также мне не хватало объяснений, почему выбран тот или иной путь. В общем, написано было сразу для умных, а не для меня. Потому я решил описать свой опыт создания простого бота надеюсь, кому-нибудь это поможет быстрее въехать в тему.

Что в статье есть, чего нет


В статье есть про:

  • создание бекенда не-инлайн бота на Java 11 с использованием Telegram Bot Api 5.0;
  • обработка команд вида /dosomething;
  • обработка текстовых сообщений, не являющихся командами (т.е. не начинающихся с "/");
  • отправку пользователю текстовых сообщений и файлов;
  • деплой и запуск бота на heroku.

В статье нет про:

  • использование функций ботов, не перечисленных выше (например, создание клавиатур я их сначала добавил, но в итоге они мне просто не пригодились);
  • создание списка заданий;
  • работу с Word-документом;
  • обеспечивающие функции логирование, тесты и т.п.;
  • общение с BotFather (создание бота, получение его токена и формирование списка команд подробно и понятно описано во многих источниках, вот первый попавшийся мануал).

Из примеров кода в статье эти функции исключены, чтобы упростить восприятие. Полностью исходный код лежит на GitHub. Если у вас вдруг есть вопросы, пишите в личку, с удовольствием проконсультирую.

Бизнес-функции бота


Очень кратко, чтобы проще было воспринимать код. Бот позволяет:

  • выдавать пользователю справочную текстовую информацию в ответ на команды /start, /help и /settings;
  • обрабатывать и запоминать пользовательские настройки, направленные текстовым сообщением заданного формата. Настроек три минимальное + максимальное число, используемые в заданиях, и количество страниц выгружаемого файла;
  • оповещать пользователя о несоблюдении им формата сообщения;
  • формировать Word-файл с заданиями на сложение, вычитание или вперемешку в ответ на команды /plus, /minus и /plusminus с использованием дефолтных или установленных пользователем настроек.

Можно потыкать MentalCalculationBot (должен работать). Выглядит так:



Общий порядок действий


  • разобраться с зависимостями;
  • создать класс бота и реализовать обработку текстовых сообщений пользователя, не являющихся командами;
  • создать классы команд;
  • прописать запуск приложения;
  • задеплоить на heroku.

Ниже подробно расписан каждый пункт.

Зависимости


Для управления зависимостями использовался Apache Maven. Нужные зависимости собственно Telegram Bots и Lombok, использовавшийся для упрощения кода (заменяет стандартные java-методы аннотациями).

Вот что вышло в
pom.xml
    <groupId>***</groupId>    <artifactId>***</artifactId>    <version>1.0-SNAPSHOT</version>    <name>***</name>    <description>***</description>    <packaging>jar</packaging>    <properties>        <java.version>11</java.version>        <maven.compiler.source>${java.version}</maven.compiler.source>        <maven.compiler.target>${java.version}</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <org.projectlombok.version>1.18.16</org.projectlombok.version>        <apache.poi.version>4.1.2</apache.poi.version>        <telegram.version>5.0.1</telegram.version>    </properties>    <dependencies>        <!-- Telegram API -->        <dependency>            <groupId>org.telegram</groupId>            <artifactId>telegrambots</artifactId>            <version>${telegram.version}</version>        </dependency>        <dependency>            <groupId>org.telegram</groupId>            <artifactId>telegrambotsextensions</artifactId>            <version>${telegram.version}</version>        </dependency>        <!-- Lombok -->        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>${org.projectlombok.version}</version>            <scope>compile</scope>        </dependency>    </dependencies>    <build>        <finalName>${project.artifactId}</finalName>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <version>3.8.1</version>                <configuration>                    <release>${java.version}</release>                    <annotationProcessorPaths>                        <path>                            <groupId>org.projectlombok</groupId>                            <artifactId>lombok</artifactId>                            <version>${org.projectlombok.version}</version>                        </path>                    </annotationProcessorPaths>                </configuration>            </plugin>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-dependency-plugin</artifactId>                <version>3.1.2</version>                <executions>                    <execution>                        <id>copy-dependencies</id>                        <phase>package</phase>                        <goals>                            <goal>copy-dependencies</goal>                        </goals>                    </execution>                </executions>            </plugin>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-surefire-plugin</artifactId>                <version>3.0.0-M5</version>            </plugin>        </plugins>    </build>


Класс бота и обработка текстовых сообщений


Мой класс Bot унаследован от TelegramLongPollingCommandBot, который, в свою очередь, наследуется от более распространённого в примерах TelegramLongPollingBot.

CommandBot хорош тем, что в нём уже реализованы приём и обработка команд то есть сообщений, начинающихся с "/". Можно создавать отдельные классы команд (подробнее о них ниже), инициализировать их в конструкторе бота и уже в них писать логику их обработки.

В классе Bot таким образом остаётся только логика обработки текстовых сообщений, не являющихся командами. В моём случае это пользовательские настройки или мусорные сообщения, не соответствующие формату. Для лаконичности логику их обработки тоже стоит вынести в отдельный вспомогательный класс, вызывая его метод из переопределенного метода processNonCommandUpdate(Update update) класса Bot.

В качестве параметров для инициализации бота используются его имя и токен, полученные от BotFather. В ходе разработки удобно создать тестового бота и прописать его параметры прямо в коде, чтобы проще было запускать бекенд локально и отлаживать прямо в Telegram, однако перед релизом стоит пересоздать бота и вынести эти настройки из кода например, в переменные окружения (об этом ниже).

Получился вот такой
Bot.java
import lombok.Getter;import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.api.objects.Message;import org.telegram.telegrambots.meta.api.objects.Update;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.MinusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.PlusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.PlusMinusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.HelpCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.SettingsCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.StartCommand;import ru.taksebe.telegram.mentalCalculation.telegram.nonCommand.NonCommand;import ru.taksebe.telegram.mentalCalculation.telegram.nonCommand.Settings;import java.util.HashMap;import java.util.Map;public final class Bot extends TelegramLongPollingCommandBot {    private final String BOT_NAME;    private final String BOT_TOKEN;    //Класс для обработки сообщений, не являющихся командой    private final NonCommand nonCommand;    /**     * Настройки файла для разных пользователей. Ключ - уникальный id чата     */    @Getter    private static Map<Long, Settings> userSettings;    public Bot(String botName, String botToken) {        super();        this.BOT_NAME = botName;        this.BOT_TOKEN = botToken;        //создаём вспомогательный класс для работы с сообщениями, не являющимися командами        this.nonCommand = new NonCommand();        //регистрируем команды        register(new StartCommand("start", "Старт"));        register(new PlusCommand("plus", "Сложение"));        register(new MinusCommand("minus", "Вычитание"));        register(new PlusMinusCommand("plusminus", "Сложение и вычитание"));        register(new HelpCommand("help","Помощь"));        register(new SettingsCommand("settings", "Мои настройки"));        userSettings = new HashMap<>();    }    @Override    public String getBotToken() {        return BOT_TOKEN;    }    @Override    public String getBotUsername() {        return BOT_NAME;    }    /**     * Ответ на запрос, не являющийся командой     */    @Override    public void processNonCommandUpdate(Update update) {        Message msg = update.getMessage();        Long chatId = msg.getChatId();        String userName = getUserName(msg);        String answer = nonCommand.nonCommandExecute(chatId, userName, msg.getText());        setAnswer(chatId, userName, answer);    }    /**     * Формирование имени пользователя     * @param msg сообщение     */    private String getUserName(Message msg) {        User user = msg.getFrom();        String userName = user.getUserName();        return (userName != null) ? userName : String.format("%s %s", user.getLastName(), user.getFirstName());    }    /**     * Отправка ответа     * @param chatId id чата     * @param userName имя пользователя     * @param text текст ответа     */    private void setAnswer(Long chatId, String userName, String text) {        SendMessage answer = new SendMessage();        answer.setText(text);        answer.setChatId(chatId.toString());        try {            execute(answer);        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя userName        }    }}


Класс обработки текстовых сообщений
NonCommand.java
import ru.taksebe.telegram.mentalCalculation.exceptions.IllegalSettingsException;import ru.taksebe.telegram.mentalCalculation.telegram.Bot;/** * Обработка сообщения, не являющегося командой (т.е. обычного текста не начинающегося с "/") */public class NonCommand {    public String nonCommandExecute(Long chatId, String userName, String text) {        Settings settings;        String answer;        try {            //создаём настройки из сообщения пользователя            settings = createSettings(text);            //добавляем настройки в мапу, чтобы потом их использовать для этого пользователя при генерации файла            saveUserSettings(chatId, settings);            answer = "Настройки обновлены. Вы всегда можете их посмотреть с помощью /settings";            //логируем событие, используя userName        } catch (IllegalSettingsException e) {            answer = e.getMessage() +                    "\n\n Настройки не были изменены. Вы всегда можете их посмотреть с помощью /settings";            //логируем событие, используя userName        } catch (Exception e) {            answer = "Простите, я не понимаю Вас. Возможно, Вам поможет /help";            //логируем событие, используя userName        }        return answer;    }    /**     * Создание настроек из полученного пользователем сообщения     * @param text текст сообщения     * @throws IllegalArgumentException пробрасывается, если сообщение пользователя не соответствует формату     */    private Settings createSettings(String text) throws IllegalArgumentException {        //отсекаем файлы, стикеры, гифки и прочий мусор        if (text == null) {            throw new IllegalArgumentException("Сообщение не является текстом");        }        //создаём из сообщения пользователя 3 числа-настройки (min, max, listCount) либо пробрасываем исключение о несоответствии сообщения требуемому формату        return new Settings(min, max, listCount);    }    /**     * Добавление настроек пользователя в мапу, чтобы потом их использовать для этого пользователя при генерации файла     * Если настройки совпадают с дефолтными, они не сохраняются, чтобы впустую не раздувать мапу     * @param chatId id чата     * @param settings настройки     */    private void saveUserSettings(Long chatId, Settings settings) {        if (!settings.equals(Settings.getDefaultSettings())) {            Bot.getUserSettings().put(chatId, settings);        }    }}


Классы команд


Все классы команд наследуются от BotCommand.

Команды в моём боте делятся на 2 группы:

  • Сервисные возвращают справочную информацию;
  • Основные формируют файл с заданиями.

Структура классов команд для этих групп идентична в абстрактном суперклассе реализуются общие методы, в наследуемые от него классах отдельных команд вынесена их кастомная логика. Разница лишь в том, что Основные команды обращаются к внешним классам, где реализована бизнес-логика, в то время как Сервисные просто формируют текстовый ответ.

Начнём с более простых Сервисных команд. В абстрактный суперкласс вынесен метод отправки пользователю ответа, а в классах команд формируется текст ответа.

Абстрактный суперкласс Сервисных команд
ServiceCommand.java
import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.bots.AbsSender;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;/** * Суперкласс для сервисных команд */abstract class ServiceCommand extends BotCommand {    ServiceCommand(String identifier, String description) {        super(identifier, description);    }    /**     * Отправка ответа пользователю     */    void sendAnswer(AbsSender absSender, Long chatId, String commandName, String userName, String text) {        SendMessage message = new SendMessage();        //включаем поддержку режима разметки, чтобы управлять отображением текста и добавлять эмодзи        message.enableMarkdown(true);        message.setChatId(chatId.toString());        message.setText(text);        try {            absSender.execute(message);        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }}


Класс Сервисной команды на примере
StartCommand.java
import org.telegram.telegrambots.meta.api.objects.Chat;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.bots.AbsSender;/** * Команда "Старт" */public class StartCommand extends ServiceCommand {    public StartCommand(String identifier, String description) {        super(identifier, description);    }    @Override    public void execute(AbsSender absSender, User user, Chat chat, String[] strings) {        //формируем имя пользователя - поскольку userName может быть не заполнено, для этого случая используем имя и фамилию пользователя        String userName = (user.getUserName() != null) ? user.getUserName() :                String.format("%s %s", user.getLastName(), user.getFirstName());        //обращаемся к методу суперкласса для отправки пользователю ответа        sendAnswer(absSender, chat.getId(), this.getCommandIdentifier(), userName,                "Давайте начнём! Если Вам нужна помощь, нажмите /help");    }}


В суперклассе Основных команд, помимо аналогичного метода отправки ответов, содержится формирование Word-документа.
OperationCommand.java
import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand;import org.telegram.telegrambots.meta.api.methods.send.SendDocument;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.api.objects.InputFile;import org.telegram.telegrambots.meta.bots.AbsSender;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import ru.taksebe.telegram.mentalCalculation.calculation.Calculator;import ru.taksebe.telegram.mentalCalculation.calculation.PlusMinusService;import ru.taksebe.telegram.mentalCalculation.enums.OperationEnum;import ru.taksebe.telegram.mentalCalculation.fileProcessor.WordFileProcessorImpl;import ru.taksebe.telegram.mentalCalculation.telegram.Settings;import java.io.FileInputStream;import java.io.IOException;import java.util.List;/** * Суперкласс для команд создания заданий с различными операциями */abstract class OperationCommand extends BotCommand {    private PlusMinusService service;    OperationCommand(String identifier, String description) {        super(identifier, description);        this.service = new PlusMinusService(new WordFileProcessorImpl(), new Calculator());    }    /**     * Отправка ответа пользователю     */    void sendAnswer(AbsSender absSender, Long chatId, List<OperationEnum> operations, String description, String commandName, String userName) {        try {            absSender.execute(createDocument(chatId, operations, description));        } catch (IOException | IllegalArgumentException e) {            sendError(absSender, chatId, commandName, userName);            e.printStackTrace();        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }    /**     * Создание документа для отправки пользователю     * @param chatId id чата     * @param operations список типов операций (сложение и/или вычитание)     * @param fileName имя, которое нужно присвоить файлу     */    private SendDocument createDocument(Long chatId, List<OperationEnum> operations, String fileName) throws IOException {        FileInputStream stream = service.getPlusMinusFile(operations, Bot.getUserSettings(chatId));        SendDocument document = new SendDocument();        document.setChatId(chatId.toString());        document.setDocument(new InputFile(stream, String.format("%s.docx", fileName)));        return document;    }    /**     * Отправка пользователю сообщения об ошибке     */    private void sendError(AbsSender absSender, Long chatId, String commandName, String userName) {        try {            absSender.execute(new SendMessage(chatId.toString(), "Похоже, я сломался. Попробуйте позже"));        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }}


Класс Основной команды на примере
PlusMinusCommand.java
import org.telegram.telegrambots.meta.api.objects.Chat;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.bots.AbsSender;import ru.taksebe.telegram.mentalCalculation.enums.OperationEnum;/** * Команда получение файла с заданиями на сложение и вычитание */public class PlusMinusCommand extends OperationCommand {    public PlusMinusCommand(String identifier, String description) {        super(identifier, description);    }    @Override    public void execute(AbsSender absSender, User user, Chat chat, String[] strings) {        //формируем имя пользователя - поскольку userName может быть не заполнено, для этого случая используем имя и фамилию пользователя        String userName = (user.getUserName() != null) ? user.getUserName() :                String.format("%s %s", user.getLastName(), user.getFirstName());        //обращаемся к методу суперкласса для формирования файла на сложение и вычитание (за это отвечает метод getPlusMinus() перечисления OperationEnum) и отправки его пользователю        sendAnswer(absSender, chat.getId(), OperationEnum.getPlusMinus(), this.getDescription(), this.getCommandIdentifier(), userName);    }}


Приложение


В методе main инициализируется TelegramBotsApi, в котором и регистрируется Bot.

TelegramBotsApi в качестве параметра принимает Class<? extends BotSession>. Если нет никаких заморочек с прокси, можно использовать DefaultBotSession.class.

Чтобы получать имя и токен бота как переменные окружения, необходимо использовать System.getenv().

Получаем вот такой
MentalCalculationApplication.java
import org.telegram.telegrambots.meta.TelegramBotsApi;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;import ru.taksebe.telegram.mentalCalculation.telegram.Bot;import java.util.Map;public class MentalCalculationApplication {    private static final Map<String, String> getenv = System.getenv();    public static void main(String[] args) {        try {            TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class);            botsApi.registerBot(new Bot(getenv.get("BOT_NAME"), getenv.get("BOT_TOKEN")));        } catch (TelegramApiException e) {            e.printStackTrace();        }    }}


Деплой на heroku


Для начала нужно создать в корне проекта файл Procfile и написать в него одну строку:
worker: java -Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8 -cp ./target/classes:./target/dependency/* <путь до приложения, в моём случае ru.taksebe.telegram.mentalCalculation.MentalCalculationApplication>
, где worker это тип процесса.

Если в проекте используется версия Java, отличная от 8, также необходимо создать в корне проекта файл system.properties и прописать в нём одну строку:
java.runtime.version=<версия Java>

Далее порядок такой:

  1. Регистрируемся на heroku и идём в консоль;
  2. mvn clean install;
  3. heroku login после выполнения потребуется нажать любую клавишу и залогиниться в открывшемся окне браузера;
  4. heroku create <имя приложения> создаём приложение на heroku;
  5. git push heroku master пушим в репозиторий heroku;
  6. heroku config:set BOT_NAME=<имя бота> добавляем имя бота в переменные окружения;
  7. heroku config:set BOT_TOKEN=<токен бота> добавляем токен бота в переменные окружения;
  8. heroku config:get BOT_NAME (аналогично BOT_TOKEN) убеждаемся, что переменные окружения установлены верно;
  9. heroku ps:scale worker=1 устанавливаем количество контейнеров (dynos) для типа процесса worker (ранее мы выбрали этот тип в Procfile), при этом происходит рестарт приложения;
  10. В интерфейсе управления приложением в личном кабинете на heroku переходим к логам (прячутся под кнопкой More в правом верхнем углу) и убеждаемся, что приложение запущено;
  11. Тестируем бота через Telegram.

Если вы храните код на GitHub, то в интерфейсе управления приложением в личном кабинете на heroku на вкладке Deploy вы можете в дальнейшем переключить деплой на GitHub-репозиторий (по запросу или автоматически), чтобы не пушить параллельно в два репозитория.

Вместо заключения


Как выяснилось, не только лишь все видели чудесный советский мультик про козлёнка, который учился считать до 10.
Подробнее..

Пишем telegram бота на языке R (часть 1) Создаём бота, и отправляем с его помощью сообщения в telegram

13.08.2020 10:12:29 | Автор: admin

Аудитория telegram ежедневно растёт с геометрической прогрессией, этому способствует удобство мессенджера, наличие каналов, чатов, и конечно возможность создавать ботов.


Боты могут использоваться в совершенно разных целях, от автоматизации коммуникации с вашими клиентами до управления вашими собственными задачами.


По сути через бота можно используя telegram выполнять любые операции: отправлять, либо запрашивать данные, запускать задачи на сервере, собирать информацию в базу данных, отправлять электронные письма и так далее.


Я планирую написать серию статей, о том, как на языке R работать с telegram bot API, и писать ботов под свои нужды.



В этой, первой статье мы разберёмся как создать телеграм бота, и отправлять с его помощью уведомления в telegram.


В результате у нас получится бот, который будет проверять статус последнего выполнения всех задач в планировщике заданий Windows, и отправлять вам уведомления, если какие-то завершились ошибкой.


Но цель этой серии статей заключается не в том, что бы научить вас писать бота под определённую, узкую задачу, а в целом познакомить с синтаксисом пакета telegram.bot, и примерами кода, с помощью которых вы сможете писать ботов, под решение собственных задач.


Содержание


Если вы интересуетесь анализом данных возможно вам будут интересны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.


  1. Создание телеграм бота
  2. Установка пакета для работы с телеграм ботом на R
  3. Отправка сообщений из R в Telegram
  4. Настройка расписания запуска проверки задач
  5. Заключение

Создание телеграм бота


Для начала нам необходимо создать бота. Делается это с помощью специального бота BotFather, переходим по ссылке и пишем боту /start.


После чего вы получите сообщение со списком команд:


Сообщение от BotFather
I can help you create and manage Telegram bots. If you're new to the Bot API, please see the manual (http://personeltest.ru/aways/core.telegram.org/bots).You can control me by sending these commands:/newbot - create a new bot/mybots - edit your bots [beta]Edit Bots/setname - change a bot's name/setdescription - change bot description/setabouttext - change bot about info/setuserpic - change bot profile photo/setcommands - change the list of commands/deletebot - delete a botBot Settings/token - generate authorization token/revoke - revoke bot access token/setinline - toggle inline mode (http://personeltest.ru/aways/core.telegram.org/bots/inline)/setinlinegeo - toggle inline location requests (http://personeltest.ru/aways/core.telegram.org/bots/inline#location-based-results)/setinlinefeedback - change inline feedback (http://personeltest.ru/aways/core.telegram.org/bots/inline#collecting-feedback) settings/setjoingroups - can your bot be added to groups?/setprivacy - toggle privacy mode (http://personeltest.ru/aways/core.telegram.org/bots#privacy-mode) in groupsGames/mygames - edit your games (http://personeltest.ru/aways/core.telegram.org/bots/games) [beta]/newgame - create a new game (http://personeltest.ru/aways/core.telegram.org/bots/games)/listgames - get a list of your games/editgame - edit a game/deletegame - delete an existing game

Для создания нового бота отправляем команду /newbot.


BotFather попросит вас ввести имя и логин бота.


BotFather, [25.07.20 09:39]Alright, a new bot. How are we going to call it? Please choose a name for your bot.Alexey Seleznev, [25.07.20 09:40]My Test BotBotFather, [25.07.20 09:40]Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.Alexey Seleznev, [25.07.20 09:40]@my_test_bot

Имя вы можете ввести произвольное, а логин должен заканчиваться на bot.


Если вы всё сделали правильно, то получите следующее сообщение:


Done! Congratulations on your new bot. You will find it at t.me/my_test_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.Use this token to access the HTTP API:123456789:abcdefghijklmnopqrstuvwxyzFor a description of the Bot API, see this page: https://core.telegram.org/bots/api

Далее вам понадобится полученный API токен, в моём примере это 123456789:abcdefghijklmnopqrstuvwxyz.


На этом шаге подготовительные работы по созданию бота завершены.


Установка пакета для работы с телеграм ботом на R


Я предполагаю, что у вас уже установлен язык R, и среда разработки RStudio. Если это не так, то вы можете посмотреть данный видео урок о том, как их установить.


Для работы с Telegram Bot API мы будем использовать R пакет telegram.bot.


Установка пакетов в R осуществляется функцией install.packages(), поэтому для установки нужного нам пакета используйте команду install.packages("telegram.bot").


Более подробно узнать об установке различных пакетов можно из этого видео.


После установки пакета его необходимо подключить:


library(telegram.bot)

Отправка сообщений из R в Telegram


Созданного вами бота можно найти в Telegram по заданному при создании логину, в моём случае это @my_test_bot.


Отправьте боту любое сообщение, например "Привет бот". На данный момент это нам надо для того, что бы получить id вашего с ботом чата.


Теперь в R пишем следующий код.


library(telegram.bot)# создаём экземпляр ботаbot <- Bot(token = "123456789:abcdefghijklmnopqrstuvwxyz")# Запрашиваем информацию о ботеprint(bot$getMe())# Получаем обновления бота, т.е. список отправленных ему сообщенийupdates <- bot$getUpdates()# Запрашиваем идентификатор чата# Примечание: перед запросом обновлений вы должны отправить боту сообщениеchat_id <- updates[[1L]]$from_chat_id()

Изначально мы создаём экземпляр нашего бота функцией Bot(), в качестве аргумента в неё необходимо передать полученный ранее токен. Хранить токен в коде считается не лучшей практикой, поэтому вы можете хранить его в переменной среды, и считывать его из неё. По умолчанию в пакете telegram.bot реализована поддержка переменных среды следующего наименования: R_TELEGRAM_BOT_ИМЯ_ВАШЕГО_БОТА. Вместо ИМЯ_ВАШЕГО_БОТА подставьте имя которое вы задали при создании, в моём случае будет переменная R_TELEGRAM_BOT_My Test Bot. Далее вы можете использовать сохранённый в переменной среды токен с помощью функции bot_token(), т.е. вот так:


bot <- Bot(token = bot_token("My Test Bot"))

Метод getUpdates()позволяет нам получить обновления бота, т.е. сообщения которые были ему отправлены. Метод from_chat_id(), позволяет получить идентификатор чата, из которого было отправлено сообщение. Этот идентификатор нам нужен для отправки сообщений от бота.


Помимо id чата из объекта полученного методом getUpdates() вы получаете и некоторую другую полезную информацию. Например, информацию о пользователе, отправившем сообщение.


updates[[1L]]$message$from

$id[1] 000000000$is_bot[1] FALSE$first_name[1] "Alexey"$last_name[1] "Seleznev"$username[1] "AlexeySeleznev"$language_code[1] "ru"

Итак, на данном этапе у нас уже есть всё, что необходимо для отправки сообщения от бота в телеграм. Воспользуемся методом sendMessage(), в который необходимо передать идентификатор чата, текст сообщения, и тип разметки текста сообщения. Тип разметки может быть Markdown или HTML и устанавливается аргументом parse_mode.


# Отправка сообщенияbot$sendMessage(chat_id,                text = "Привет, *жирный текст* _курсив_",                parse_mode = "Markdown")

Основы форматирования Markdown разметки:


  • Жирный шрифт выделяется с помощью знака *:
    • пример: *жирный шритф*
    • результат: жирный шритф
  • Курсив задаётся нижним подчёркиванием:
    • пример: _курсив_
    • результат: курсив
  • Моноширинный шрифт, которым обычно выделяется программный код, задаётся с помощью апострофов `:
    • пример: `моноширинный шрифт`
    • результат: моноширинный шрифт

Основы форматирования HTML разметки:
В HTML вы заворачиваете часть текста, которую надо выделать, в теги, пример <тег>текст</тег>.


  • <тег> открывающий тег
  • </тег> закрывающий тег

Теги HTML разметки


  • <b> жирный шрифт
    • пример: <b>жирный шрифт</b>
    • результат жирный шрифт
  • <i> курсив
    • пример: <i>курсив</i>
    • результат: курсив
  • <code> моноширинный шрифт
    • пример: <code>моноширинный шрифт</code>
    • результат: моноширинный шрифт

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


# Отправить изображениеbot$sendPhoto(chat_id,  photo = "https://telegram.org/img/t_logo.png")# Отправка голосового сообщенияbot$sendAudio(chat_id,  audio = "http://www.largesound.com/ashborytour/sound/brobob.mp3")# Отправить документbot$sendDocument(chat_id,  document = "https://github.com/ebeneditos/telegram.bot/raw/gh-pages/docs/telegram.bot.pdf")# Отправить стикерbot$sendSticker(chat_id,  sticker = "https://www.gstatic.com/webp/gallery/1.webp")# Отправить видеоbot$sendVideo(chat_id,  video = "http://techslides.com/demos/sample-videos/small.mp4")# Отправить gif анимациюbot$sendAnimation(chat_id,  animation = "https://media.giphy.com/media/sIIhZliB2McAo/giphy.gif")# Отправить локациюbot$sendLocation(chat_id,  latitude = 51.521727,  longitude = -0.117255)# Имитация действия в чатеbot$sendChatAction(chat_id,  action = "typing")

Т.е. например с помощью метода sendPhoto() вы можете отправить сохранённый в виде изображения график, который вы построили с помощью пакета ggplot2.


Проверка планировщика задач Windows, и отправка уведомления о задачах, работа которых была завершена аварийно


Для работы с планировщиком заданий Windows вам необходимо установить пакет taskscheduleR, и для удобства работы с данными установим пакет dplyr.


# Установка пакетовinstall.packages(c('taskscheduleR', 'dplyr'))# Подключение пакетовlibrary(taskscheduleR)library(dplyr)

Далее с помощью функции taskscheduler_ls() мы запрашиваем информацию о задачах из нашего планировщика. С помощью функции filter() из пакета dplyr мы убираем из списка задач те, которые были успешно выполненны и имеют статус последнего результата 0, и те, которые ещё ни разу не запускались и имеют статус 267011, выключенные задачи, и задачи которые выполняются в данный момент.


# запрашиваем список задачtask <- task <- taskscheduler_ls() %>%        filter(! `Last Result`  %in% c("0", "267011") &                `Scheduled Task State` == "Enabled" &                Status != "Running") %>%        select(TaskName) %>%        unique() %>%        unlist() %>%        paste0(., collapse = "\n")

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


Если рассмотреть каждую команду подробнее, то:


  • filter() фильтрует список задач, по описанным выше условиям
  • select() оставляет в таблице только одно поле с названием задач
  • unique() убирает дубли названий
  • unlist() переводит выбранный столбец таблицы в вектор
  • paste0() соединяет названия задач в одну строку, и ставит в качестве разделителя знак перевода строки, т.е. \n.

Всё что нам остаётся отправить этот результат в телеграм.


bot$sendMessage(chat_id,                text = task,                parse_mode = "Markdown")

Итак, на данный момент код бота выглядит вот так:


Код бота проверяющего задачи
# Подключение пакетаlibrary(telegram.bot)library(taskscheduleR)library(dplyr)# инициализируем ботаbot <- Bot(token = "123456789:abcdefghijklmnopqrstuvwxyz")# идентификатор чатаchat_id <- 123456789# запрашиваем список задачtask <- taskscheduler_ls() %>%        filter(! `Last Result`  %in% c("0", "267011")  &               `Scheduled Task State` == "Enabled" &                Status != "Running") %>%        select(TaskName) %>%        unique() %>%        unlist() %>%        paste0(., collapse = "\n")# если есть проблемные задачи отправляем сообщениеif ( task != "" ) {  bot$sendMessage(chat_id,                  text = task,                  parse_mode = "Markdown"  )}

При использовании приведённого выше примера подставьте в код токен вашего бота и ваш идентификатор чата.


Вы можете добавлять условия фильтрации задач, например проверяя только те задачи, которые были созданны вами, исключая системные.


Так же вы можете вынести различные настройки в отдельный файл конфигурации, и хранить в нём id чата и токен. Читать конфиг можно например с помощью пакета configr.


Пример ini конфига
[telegram_bot];настройки телеграм бота и чата, в который будут приходить уведомленияchat_id=12345678bot_token=123456789:abcdefghijklmnopqrstuvwxyz"

Пример чтения переменных из конфига в R
library(configr)# чтение конфинаconfig <- read.config('C:/путь_к_конфигу/config.cfg', rcmd.parse = TRUE)bot_token <- config$telegram_bot$bot_tokenchat_id     <- config$telegram_bot$chat_id

Настраиваем расписание запуска проверки задач


Наиболее подробно процесс настройки запуска скриптов по расписанию описан в этой статье. Тут я лишь опишу шаги, которые для этого необходимо выполнить. Если какой-то из шагов вам не понятен, то обратитесь к статье на которую я указал ссылку.


Предположим, что мы сохранили код нашего бота в файл check_bot.R. Для того, что бы запланировать регулярный запуск этого файла выполните следующие шаги:


  1. Пропишите в системную переменную Path путь к папке в которой установлен R, в Windows путь будет примерно таким: C:\Program Files\R\R-4.0.2\bin.
  2. Создайте исполняемый bat файл, в котором будет всего одна строка R CMD BATCH C:\rscripts\check_bot\check_bot.R. Замените C:\rscripts\check_bot\check_bot.R на полный путь к вашему R файлу.
  3. Далее настройте с помощью планировщика задач Windows расписание запуска, например на каждые пол часа.

Заключение


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


Я описал задачу контроля планировщика заданий Windows, но вы можете использовать материал этой статьи для отправки любых уведомлений, от прогноза погоды до котировок акций на фондовой бирже, т.к. R позволяет вам подключиться к огромному количеству источников данных.


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

Подробнее..

Пишем telegram бота на языке R (часть 2) Добавляем боту поддержку команд и фильтры сообщений

25.08.2020 10:05:02 | Автор: admin

В предыдущей публикации мы разобрались как создать бота, инициализировали экземпляр класса Bot и ознакомились с методами отправки сообщений с его помощью.


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


В этот раз мы разберёмся как оживить нашего бота и добавим ему поддержку команд, а также познакомимся с классом Updater.


В ходе статьи мы напишем нескольких простых ботов, последний будет по заданной дате и коду страны определять является ли день в данной стране выходным или рабочим согласно производственного календаря. Но, как и прежде цель статьи ознакомить вас с интерфейсом пакета telegram.bot для решения ваших собственных задач.



Все статьи из серии "Пишем telegram бота на языке R"


  1. Создаём бота, и отправляем с его помощью сообщения в telegram
  2. Добавляем боту поддержку команд и фильтры сообщений

Содержание


  1. Класс Updater
  2. Handlers обработчики
  3. Добавляем первую команду боту, обработчик команд
  4. Обработчик текстовых сообщений и фильтры
  5. Добавление команд с параметрами
  6. Запускаем бота в фоновом режиме
  7. Заключение

Класс Updater


Updater это класс, который упрощает вам разработку телеграм бота, и использует под капотом класс Dispetcher. Назначение класса Updater заключается в том, что бы получить обновления от бота (в предыдущей статье мы использовали для этой цели метод getUpdates()), и передать их далее в Dispetcher.


В свою очередь Dispetcher содержит в себе созданные вами обработчики, т.е. объекты класса Handler.


Handlers обработчики


С помощью обработчиков вы добавляете в Dispetcher реакции бота на различные события. На момент написания статьи в telegram.bot добавлены следующие типы обработчиков:


  • MessageHandler Обработчик сообщений
  • CommandHandler Обработчик команд
  • CallbackQueryHandler Обработчик данных отправляемых из Inline клавиатур
  • ErrorHandler Обработчик ошибок при запросе обновлений от бота

Добавляем первую команду боту, обработчик команд


Если вы никогда ранее не использовали ботов, и не в курсе, что такое команда, то команды боту необходимо отправлять с помощью прямого слеша / в качестве префикса.


Начнём мы с простых команд, т.е. научим нашего бота здороваться по команде /hi.


Код 1: Учим бота здороваться
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# Пишем метод для приветсвияsay_hello <- function(bot, update) {  # Имя пользователя с которым надо поздароваться  user_name <- update$message$from$first_name  # Отправка приветственного сообщения  bot$sendMessage(update$message$chat_id,                   text = paste0("Моё почтение, ", user_name, "!"),                   parse_mode = "Markdown")}# создаём обработчик hi_hendler <- CommandHandler('hi', say_hello)# добаляем обработчик в диспетчерupdater <- updater + hi_hendler# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

Метод start_polling() класса Updater, который используется в конце кода, запускает бесконечный цикл запроса и обработки обновлений от бота.


Теперь откроем телеграм, и напишем нашему боту первую команду /hi.



Теперь наш бот понимает команду /hi, и умеет с нами здороваться.


Схематически процесс построения такого простейшего бота можно изобразить следующим образом.



  1. Создаём экземпляр класса Updater;
  2. Создаём методы, т.е. функции которые будет выполнять наш бот. В примере кода это функция say_hello(). Функции, которые вами будут использоваться как методы бота должны иметь два обязательных аргумента bot и update, и один необязательный args. Аргумент bot, это и есть ваш бот, с его помощью вы можете отвечать на сообщения, отправлять сообщения, или использовать любые другие доступные боту методы. Аргумент update это то, что бот получил от пользователя, по сути, то что в первой статье мы получали методом getUpdates(). Аргумент args позволяет вам обрабатывать дополнительные данные отправленные пользователем вместе с командой, к этой теме мы ещё вернёмся немного позже;
  3. Создаём обработчики, т.е. связываем какие-то действия пользователя с созданными на прошлом шаге методами. По сути обработчик это триггер, событие которое вызывает какую-то функцию бота. В нашем примере таким триггером является отправка команды /hi, и реализуется командой hi_hendler <- CommandHandler('hi', say_hello). Первый аргумент функции CommandHandler() позволяет вам задать команду, в нашем случае hi, на которую будет реагировать бот. Второй аргумент позволяет указать метод бота, мы будем вызывать метод say_hello, который будет выполняться если пользователь вызвал указанную в первом аргументе команду;
  4. Далее добавляем созданный обработчик в диспетчер нашего экземпляра класса Updater. Добавлять обработчики можно несколькими способами, в примере выше я использовал простейший, с помощью знака +, т.е. updater <- updater + hi_hendler. То же самое можно сделать с помощью метода add_handler(), который относится к классу Dispatcher, найти этот метод можно так: updater$dispatcher$add_handler();
  5. Запускаем бота с помощью команды start_polling().

Обработчик текстовых сообщений и фильтры


Как отправлять боту команды мы разобрались, но иногда нам требуется, что бы бот реагировал не только на команды, но и на какие-то обычные, текстовые сообщения. Для этого необходимо использовать обработчики сообщений MessageHandler.


Обычный MessageHandler будет реагировать на абсолютно все входящие сообщения. Поэтому зачастую обработчики сообщений используются вместе с фильтрами. Давайте научим бота здороваться не только по команде /hi, но и всегда, когда в сообщении отправленном боту встречается одно из следующих слов: привет, здравствуй, салют, хай, бонжур.


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


Код 2: Добавляем обработчик текстовых сообщений и фильтр
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# Пишем метод для приветсвия## команда приветвияsay_hello <- function(bot, update) {  # Имя пользователя с которым надо поздароваться  user_name <- update$message$from$first_name  # Отправляем приветсвенное сообщение  bot$sendMessage(update$message$chat_id,                   text = paste0("Моё почтение, ", user_name, "!"),                  parse_mode = "Markdown",                  reply_to_message_id = update$message$message_id)}# создаём фильтрыMessageFilters$hi <- BaseFilter(function(message) {  # проверяем, встречается ли в тексте сообщения слова: привет, здравствуй, салют, хай, бонжур  grepl(x           = message$text,         pattern     = 'привет|здравствуй|салют|хай|бонжур',        ignore.case = TRUE)  })# создаём обработчик hi_hendler <- CommandHandler('hi', say_hello) # обработчик команды hihi_txt_hnd <- MessageHandler(say_hello, filters = MessageFilters$hi)# добаляем обработчики в диспетчерupdater <- updater +              hi_hendler +             hi_txt_hnd# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

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


Итак, в первую очередь мы научили бота не просто здороваться, а отвечать на приветствие. Сделали мы это с помощью аргумента reply_to_message_id, который доступен в методе sendMessage(), в который необходимо передать id сообщения на которое требуется ответить. Получить id сообщения можно вот так: update$message$message_id.


Но главное, что мы сделали добавили боту фильтр с помощью функции BaseFilter():


# создаём фильтрыMessageFilters$hi <- BaseFilter(   # анонимная фильтрующая функция  function(message) {    # проверяем, встречается ли в тексте сообщения слова приветствия    grepl(x           = message$text,           pattern     = 'привет|здравствуй|салют|хай|бонжур',          ignore.case = TRUE)  })

Как вы могли заметить, фильтры необходимо добавлять в объект MessageFilters, в котором изначально уже есть небольшой набор готовых фильтров. В нашем примере в объект MessageFilters мы добавили элемент hi, это новый фильтр.


В функцию BaseFilter() вам необходимо передать фильтрующую функцию. По сути, фильтр это просто функция, которая получает экземпляр сообщения и возвращает TRUE или FALSE. В нашем примере, мы написали простейшую функцию, которая с помощью базовой функции grepl() проверяет текст сообщения, и если он соответствует регулярному выражению привет|здравствуй|салют|хай|бонжур возвращает TRUE.


Далее мы создаём обработчик сообщений hi_txt_hnd <- MessageHandler(say_hello, filters = MessageFilters$hi). Первый аргумент функции MessageHandler() метод, который будет вызывать обработчик, а второй аргумент это фильтр по которому он будет вызываться. В нашем случае это созданный нами фильтр MessageFilters$hi.


Ну и в итоге, мы добавляем в диспетчер созданный только, что обработчик hi_txt_hnd.


updater <- updater +              hi_hendler +             hi_txt_hnd

Как я уже писал выше, в пакете telegram.bot и объекте MessageFilters уже есть набор встроенных фильтров, которые вы можете использовать:


  • all Все сообщения
  • text Текстовые сообщения
  • command Команды, т.е. сообщения которые начинаются на /
  • reply Сообщения, которые являются ответом на другое сообщение
  • audio Сообщения в которых содержится аудио файл
  • document Сообщения с отправленным документом
  • photo Сообщения с отправленными изображениями
  • sticker Сообщения с отправленным стикером
  • video Сообщения с видео
  • voice Голосовые сообщения
  • contact Сообщения в которых содержится контант телеграм пользователя
  • location Сообщения с геолокацией
  • venue Пересылаемые сообщения
  • game Игры

Если вы хотите совместить некоторые фильтры в одном обработчике просто используйте знак | в качестве логического ИЛИ, и знак & в качестве логического И. Например, если вы хотите что бы бот вызывал один и тот же метод когда он получает видео, изображение или документ используйте следующий пример создания обработчика сообщений:


handler <- MessageHandler(callback,   MessageFilters$video | MessageFilters$photo | MessageFilters$document)

Добавление команд с параметрами


Мы уже знаем, что такое команды, как их создавать и как заставить бота выполнить нужную команду. Но в некоторых случаях помимо названия команды, нам необходимо передать некоторые данные для её выполнения.


Ниже пример бота, который по заданной дате и стране возвращает вам тип дня из производственного календаря.


Приведённый ниже бот использует API производственного календаря isdayoff.ru.


Код 3: Бот, который сообщает по дате и стране
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('1165649194:AAFkDqIzQ6Wq5GV0YU7PmEZcv1gmWIFIB_8')# Пишем метод для приветсвия## команда приветвияcheck_date <- function(bot, update, args) {  # входящие данные  day     <- args[1]  # дата  country <- args[2]  # страна  # проверка введённых параметров  if ( !grepl('\\d{4}-\\d{2}-\\d{2}', day) ) {    # Send Custom Keyboard    bot$sendMessage(update$message$chat_id,                     text = paste0(day, " - некорреткная дата, введите дату в формате ГГГГ-ММ-ДД"),                    parse_mode = "Markdown")  } else {    day <- as.Date(day)    # переводим в формат POSIXtl    y <- format(day, "%Y")    m <- format(day, "%m")    d <- format(day, "%d")  }  # страна для проверки  ## проверяем задана ли страна  ## если не задана устанавливаем ru  if ( ! country %in% c('ru', 'ua', 'by', 'kz', 'us') ) {    # Send Custom Keyboard    bot$sendMessage(update$message$chat_id,                     text = paste0(country, " - некорретктный код страны, возможнные значения: ru, by, kz, ua, us. Запрошены данные по России."),                    parse_mode = "Markdown")    country <- 'ru'  }  # запрос данных из API  # компоновка HTTP запроса  url <- paste0("https://isdayoff.ru/api/getdata?",                "year=",  y, "&",                "month=", m, "&",                "day=",   d, "&",                "cc=",    country, "&",                "pre=1&",                "covid=1")  # получаем ответ  res <- readLines(url)  # интрепретация ответа  out <- switch(res,                 "0"   = "Рабочий день",                "1"   = "Нерабочий день",                "2"   = "Сокращённый рабочий день",                "4"   = "covid-19",                "100" = "Ошибка в дате",                "101" = "Данные не найдены",                "199" = "Ошибка сервиса")  # отправляем сообщение  bot$sendMessage(update$message$chat_id,                   text = paste0(day, " - ", out),                  parse_mode = "Markdown")}# создаём обработчик date_hendler <- CommandHandler('check_date', check_date, pass_args = TRUE)# добаляем обработчик в диспетчерupdater <- updater + date_hendler# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

Мы создали бота, который в арсенале имеет всего один метод check_date, данный метод вызывается одноимённой командой.


Но, помимо имени команды, данный метод ждёт от вас введения двух параметров, код страны и дату. Далее бот проверяется, является ли заданный день в указанной стране выходным, сокращённым или рабочим согласно официального производственного календаря.


Что бы создаваемый нами метод принимал дополнительные параметры вместе с командой, используйте аргумент pass_args = TRUE в функции CommandHandler(), и при создании метода, помимо обязательных аргументов bot, update создайте опциональный args. Созданный таким образом метод будет принимать параметры, которые вы передаёте боту после названия команды. Параметры необходимо между собой разделять пробелом, в метод они поступят в виде текстового вектора.


Давайте запустим, и протестируем нашего бота.



Запускаем бота в фоновом режиме


Последний шаг который нам осталось выполнить запустить бота в фоновом режиме.


Для этого следуйте по описанному ниже алгоритму:


  1. Сохраните код бота в файл с расширением R. При работе в RStudio это делается через меню File, командой Save As....
  2. Добавьте путь к папке bin, которая в свою очередь находится в папке в которую вы установили язык R в переменную Path, инструкция тут.
  3. Создайте обычный текстовый файл, в котором пропишите 1 строку: R CMD BATCH C:\Users\Alsey\Documents\my_bot.R. Вместо C:\Users\Alsey\Documents\my_bot.R пропишите путь к своему скрипту бота. При этом важно, что бы в пути не встречалась кириллица и пробелы, т.к. это может вызвать проблемы при запуске бота. Сохраните его, и замените его расширение с txt на bat.
  4. Откройте планировщик заданий Windows, есть множество способов это сделать, например откройте любую папку и в адресс введите %windir%\system32\taskschd.msc /s. Другие способы запуска можно найти тут.
  5. В верхнем правом меню планировщика нажмите "Создать задачу...".
  6. На вкладке "Общие" задайте произвольное имя вашей задаче, и переключатель перевидите в состояние "Выполнять для всех пользователей".
  7. Перейдите на вкладку "Действия", нажмите "Создать". В поле "Программа или сценарий" нажмите "Обзор", найдите созданный на втором шаге bat файл, и нажмите ОК.
  8. Жмём ОК, при необходимости вводим пароль от вашей учётной записи операционной системы.
  9. Находим в планировщике созданную задачу, выделяем и в нижнем правом углу жмём кнопку "Выполнить".

Наш бот запущен в фоновом режиме, и будет работать до тех пор, пока вы не остановите задачу, или не выключите ваш ПК или сервер на котором его запустили.


Заключение


В этой статье мы разобрались как написать полноценного бота, который не только умеет отправлять сообщения, но и реагировать на входящие сообщения и команды. Полученных знананий уже достаточно для решения большинства ваших задач.


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

Подробнее..

Пишем telegram бота на языке R (часть 3) Как добавить боту поддержку клавиатуры

08.09.2020 10:16:23 | Автор: admin

Это третья статья из серии "Пишем telegram бота на языке R". В предыдущих публикациях мы научились создавать телеграм бота, отправлять через него сообщения, добавили боту команды и фильтры сообщений. Поэтому перед тем как приступить к чтению данной статьи я крайне рекомендую ознакомиться с предыдущими, т.к. тут я уже не буду останавливать на описанных ранее основах ботостроения.


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



Все статьи из серии "Пишем telegram бота на языке R"


  1. Создаём бота, и отправляем с его помощью сообщения в telegram
  2. Добавляем боту поддержку команд и фильтры сообщений
  3. Как добавить боту поддержку клавиатуры

Содержание


Если вы интересуетесь анализом данных возможно вам будут интересны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.


  1. Какие типы клавиатур поддерживает телеграм бот
  2. Reply клавиатура
  3. Inline клавиатура
    3.1. Пример простейшего бота с поддержкой InLine кнопок
    3.2. Пример бота, который сообщает текущую погоду по выбранному городу
    3.3. Пример бота, который выводит список самых свежих статей со ссылками по-указанному Хабу из habr.com
  4. Заключение

Какие типы клавиатур поддерживает телеграм бот


На момент написания статьи telegram.bot позволяет вам создать клавиатуры двух типов:


  • Reply Основная, обычная клавиатура, которая находится под панелью ввода текста сообщения. Такая клавиатура просто отправляет боту текстовое сообщение, и в качестве текста отправит тот текст, который написан на самой кнопке.
  • Inline Клавиатура привязанная к конкретному сообщению бота. Данная клавиатура отправляет боту данные, привязанные к нажатой кнопке, эти данные могут отличаться от текста, написанного на самой кнопке. И обрабатываются такие кнопки через CallbackQueryHandler.

Для того, что бы бот открыл клавиатуру необходимо при отправке сообщения через метод sendMessage(), передать созданную ранее клавиатуру в аргумент reply_markup.


Ниже мы разберём несколько примеров.


Reply клавиатура


Как я уже писал выше, это основная клавиатура управления ботом.


Пример создания Reply клавиатуры из официальной справки
bot <- Bot(token = "TOKEN")chat_id <- "CHAT_ID"# Create Custom Keyboardtext <- "Aren't those custom keyboards cool?"RKM <- ReplyKeyboardMarkup(  keyboard = list(    list(KeyboardButton("Yes, they certainly are!")),    list(KeyboardButton("I'm not quite sure")),    list(KeyboardButton("No..."))  ),  resize_keyboard = FALSE,  one_time_keyboard = TRUE)# Send Custom Keyboardbot$sendMessage(chat_id, text, reply_markup = RKM)

Выше приведён пример из официальной справки пакета telegram.bot. Для создания клавиатуры используется функция ReplyKeyboardMarkup(), которая в свою очередь принимает список списков кнопок, которые создаются функцией KeyboardButton().


Почему в ReplyKeyboardMarkup() необходимо передавать не просто список, а список списков? Дело в том, что вы передаёте основной список, и в нём отдельными списками вы задаёте каждый ряд кнопок, т.к. в один ряд можно расположить несколько кнопок.


Аргумент resize_keyboard позволяет автоматически подбирать оптимальный размер кнопок клавиатуры, а аргумент one_time_keyboard позволяет скрывать клавиатуру после каждого нажатия на кнопку.


Давайте напишем простейшего бота, у которого будет 3 кнопки:


  • Чат ID Запросить чат ID диалога с ботом
  • Моё имя Запросить своё имя
  • Мой логин Запросить своё имя пользователя в телеграм

Код 1: Простой бот с Reply клавиатурой
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# создаём методы## метод для запуска клавиатурыstart <- function(bot, update) {  # создаём клавиатуру  RKM <- ReplyKeyboardMarkup(    keyboard = list(      list(KeyboardButton("Чат ID")),      list(KeyboardButton("Моё имя")),      list(KeyboardButton("Мой логин"))    ),    resize_keyboard = FALSE,    one_time_keyboard = TRUE  )  # отправляем клавиатуру  bot$sendMessage(update$message$chat_id,                  text = 'Выберите команду',                   reply_markup = RKM)}## метод возвразающий id чатаchat_id <- function(bot, update) {  bot$sendMessage(update$message$chat_id,                   text = paste0("Чат id этого диалога: ", update$message$chat_id),                  parse_mode = "Markdown")}## метод возвращающий имяmy_name <- function(bot, update) {  bot$sendMessage(update$message$chat_id,                   text = paste0("Вас зовут ", update$message$from$first_name),                  parse_mode = "Markdown")}## метод возвращающий логинmy_username <- function(bot, update) {  bot$sendMessage(update$message$chat_id,                   text = paste0("Ваш логин ", update$message$from$username),                  parse_mode = "Markdown")}# создаём фильтры## сообщения с текстом Чат IDMessageFilters$chat_id <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Чат ID"})## сообщения с текстом Моё имяMessageFilters$name <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Моё имя"})## сообщения с текстом Мой логинMessageFilters$username <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Мой логин")# создаём обработчикиh_start    <- CommandHandler('start', start)h_chat_id  <- MessageHandler(chat_id, filters = MessageFilters$chat_id)h_name     <- MessageHandler(my_name, filters = MessageFilters$name)h_username <- MessageHandler(my_username, filters = MessageFilters$username)# добавляем обработчики в диспетчерupdater <- updater +             h_start +            h_chat_id +            h_name +            h_username# запускаем бота updater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

После запуска задайте боту команду /start, т.к. именно её мы определили для запуска клавиатуры.



Если на данный момент вам сложно разобрать приведённый пример кода, с созданием методов, фильтров и обработчиков, то следует вернуться к предыдущей статье, в которой я подробно всё это описал.


Мы создали 4 метода:


  • start Запуск клавиатуры
  • chat_id Запрос идентификатора чата
  • my_name Запрос своего имени
  • my_username Запрос своего логина

В объект MessageFilters добавили 3 фильтра сообщений, по их тексту:


  • chat_id Сообщения с текстом "Чат ID"
  • name Сообщения с текстом "Моё имя"
  • username Сообщения с текстом "Мой логин"

И создали 4 обработчика, которые по заданным командам и фильтрам будут выполнять указанные методы.


# создаём обработчикиh_start    <- CommandHandler('start', start)h_chat_id  <- MessageHandler(chat_id, filters = MessageFilters$chat_id)h_name     <- MessageHandler(my_name, filters = MessageFilters$name)h_username <- MessageHandler(my_username, filters = MessageFilters$username)

Сама клавиатура создаётся внутри метода start() командой ReplyKeyboardMarkup().


RKM <- ReplyKeyboardMarkup(    keyboard = list(      list(KeyboardButton("Чат ID")),      list(KeyboardButton("Моё имя")),      list(KeyboardButton("Мой логин"))    ),    resize_keyboard = FALSE,    one_time_keyboard = TRUE)

В нашем случае все кнопки мы расположили друг под другом, но мы можем расположить их в один ряд, внеся изменения в список списков кнопок. Т.к. один ряд внутри клавиатуры создаётся через вложенный список кнопок, то для того, что бы вывести наши кнопки в один ряд надо переписать часть кода по построению клавиатуры вот так:


RKM <- ReplyKeyboardMarkup(    keyboard = list(      list(          KeyboardButton("Чат ID"),          KeyboardButton("Моё имя"),          KeyboardButton("Мой логин")     )    ),    resize_keyboard = FALSE,    one_time_keyboard = TRUE)


Отправляется клавиатура в чат методом sendMessage(), в аргументе reply_markup.


  bot$sendMessage(update$message$chat_id,                  text = 'Выберите команду',                   reply_markup = RKM)

Inline клавиатура


Как я уже писал выше, Inline клавиатура привязана к конкретному сообщению. С ней работать несколько сложнее чем с основной клавиатурой.


Изначально вам необходимо добавить боту метод, для вызова Inline клавиатуры.


Для ответа на нажатие Inline кнопки также можно использовать метод бота answerCallbackQuery(), который может вывести уведомление в интерфейсе telegram, пользователю нажавшему Inline кнопку.


Данные отправленные с Inline кнопки не являются текстом, поэтому для их обработки необходимо создать специальный обработчик с помощью команды CallbackQueryHandler().


Код построения Inline клавиатуры который приводится в официальной справке пакета telegram.bot.


Код построения Inline клавиатуры из официальной справки
# Initialize botbot <- Bot(token = "TOKEN")chat_id <- "CHAT_ID"# Create Inline Keyboardtext <- "Could you type their phone number, please?"IKM <- InlineKeyboardMarkup(  inline_keyboard = list(    list(      InlineKeyboardButton(1),      InlineKeyboardButton(2),      InlineKeyboardButton(3)    ),    list(      InlineKeyboardButton(4),      InlineKeyboardButton(5),      InlineKeyboardButton(6)    ),    list(      InlineKeyboardButton(7),      InlineKeyboardButton(8),      InlineKeyboardButton(9)    ),    list(      InlineKeyboardButton("*"),      InlineKeyboardButton(0),      InlineKeyboardButton("#")    )  ))# Send Inline Keyboardbot$sendMessage(chat_id, text, reply_markup = IKM)

Строить Inline клавиатуру необходимо с помощью команды InlineKeyboardMarkup(), по такому же принципу, как и Reply клавиатуру. В InlineKeyboardMarkup() необходимо передать список, списков Inline кнопок, каждая отдельная кнопка создаётся функцией InlineKeyboardButton().


Inline кнопка может либо передавать боту какие-то данные с помощью аргумента callback_data, либо открывать какую-либо HTML страницу, заданную с помощью аргумента url.


В результате будет список, в котором каждый элемент так же является списком Inline кнопок, которые необходимо объединить в один ряд.


Далее мы рассмотрим несколько примеров ботов с Inline кнопками.


Пример простейшего бота с поддержкой InLine кнопок


Для начала мы напишем бота для экспресс тестирования на covid-19. По команде /test, он будет отправлять вам клавиатуру с двумя кнопками, в зависимости от нажатой кнопки он будет присылать вам сообщение с результатами вашего тестирования.


Код 2: Простейший бот с Inline клавиатурой
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# метод для отправки InLine клавиатурыtest <- function(bot, update) {  # создаём InLine клавиатуру  IKM <- InlineKeyboardMarkup(    inline_keyboard = list(      list(        InlineKeyboardButton("Да", callback_data = 'yes'),        InlineKeyboardButton("Нет", callback_data = 'no')      )    )  )  # Отправляем клавиатуру в чат  bot$sendMessage(update$message$chat_id,                   text = "Вы болете коронавирусом?",                   reply_markup = IKM)}# метод для обработки нажатия кнопкиanswer_cb <- function(bot, update) {  # полученные данные с кнопки  data <- update$callback_query$data  # получаем имя пользователя, нажавшего кнопку  uname <- update$effective_user()$first_name  # обработка результата  if ( data == 'no' ) {    msg <- paste0(uname, ", поздравляю, ваш тест на covid-19 отрицательный.")  } else {    msg <- paste0(uname, ", к сожалени ваш тест на covid-19 положительный.")  }  # Отправка сообщения  bot$sendMessage(chat_id = update$from_chat_id(),                  text = msg)  # сообщаем боту, что запрос с кнопки принят  bot$answerCallbackQuery(callback_query_id = update$callback_query$id) }# создаём обработчикиinline_h      <- CommandHandler('test', test)query_handler <- CallbackQueryHandler(answer_cb)# добавляем обработчики в диспетчерupdater <- updater + inline_h + query_handler# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

Результат:


Мы создали два метода:


  • test Для отправки в чат Inline клавиатуры
  • answer_cb Для обработки отправленных с клавиатуры данных.

Данные, которые будут отправлены с каждой кнопки задаются в аргументе callback_data, при создании кнопки. Получить отправленные с кнопки данные можно с помощью конструкции update$callback_query$data, внутри метода answer_cb.


Что бы бот реагировал на Inline клавиатуру, метод answer_cb обрабатывается специальным обработчиком: CallbackQueryHandler(answer_cb). Который запускает указанный метод по нажатию Inline кнопки. Обработчик CallbackQueryHandler принимает два аргумента:


  • callback Метод который необходимо запустить
  • pattern Фильтр по данным, которые привязаны к кнопке с помощью аргумента callback_data.

Соответвенно с помощью аргумента pattern мы можем под нажатие каждой кнопки написать отдельный метод:


Код 3: Разделяем методы под каждую Inline кнопку
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# метод для отправки InLine клавиатурыtest <- function(bot, update) {    # создаём InLine клавиатуру  IKM <- InlineKeyboardMarkup(    inline_keyboard = list(      list(        InlineKeyboardButton("Да", callback_data = 'yes'),        InlineKeyboardButton("Нет", callback_data = 'no')      )    )  )  # Отправляем клавиатуру в чат  bot$sendMessage(update$message$chat_id,                   text = "Вы болете коронавирусом?",                   reply_markup = IKM)}# метод для обработки нажатия кнопки Даanswer_cb_yes <- function(bot, update) {  # получаем имя пользователя, нажавшего кнопку  uname <- update$effective_user()$first_name  # обработка результата  msg <- paste0(uname, ", к сожалени ваш текст на covid-19 положительный.")  # Отправка сообщения  bot$sendMessage(chat_id = update$from_chat_id(),                  text = msg)  # сообщаем боту, что запрос с кнопки принят  bot$answerCallbackQuery(callback_query_id = update$callback_query$id) }# метод для обработки нажатия кнопки Нетanswer_cb_no <- function(bot, update) {  # получаем имя пользователя, нажавшего кнопку  uname <- update$effective_user()$first_name  msg <- paste0(uname, ", поздравляю, ваш текст на covid-19 отрицательный.")  # Отправка сообщения  bot$sendMessage(chat_id = update$from_chat_id(),                  text = msg)  # сообщаем боту, что запрос с кнопки принят  bot$answerCallbackQuery(callback_query_id = update$callback_query$id) }# создаём обработчикиinline_h          <- CommandHandler('test', test)query_handler_yes <- CallbackQueryHandler(answer_cb_yes, pattern = 'yes')query_handler_no  <- CallbackQueryHandler(answer_cb_no, pattern = 'no')# добавляем обработчики в диспетчерupdater <- updater +             inline_h +             query_handler_yes +            query_handler_no# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

Теперь мы написали 2 отдельных метода, т.е. по одному методу, под нажатие каждой кнопки, и использовали аргумент pattern, при создании их обработчиков:


query_handler_yes <- CallbackQueryHandler(answer_cb_yes, pattern = 'yes')query_handler_no  <- CallbackQueryHandler(answer_cb_no, pattern = 'no')

Заканчивается код метода answer_cb командой bot$answerCallbackQuery(callback_query_id = update$callback_query$id), которая сообщает боту, что данные с inline клавиатуры получены.


Пример бота, который сообщает текущую погоду по выбранному городу


Давайте попробуем написать бота, который запрашивает данные о погоде.


Логика его работы будет следующая. Изначально командой /start вы вызываете основную клавиатуру, в которой присутствует всего одна кнопка "Погода". Нажав на эту кнопку вы получаете сообщение с Inline клавиатурой, для выбора города, по которому требуется узнать текущую погоду. Выбираете один из городов, и получаете текущую погоду.


В этом примере кода мы будем использовать несколько дополнительных пакетов:


  • httr пакет для работы с HTTP запросами, на основе которых построена работа с любым API. В нашем случае мы будем использовать бесплатный API openweathermap.org.
  • stringr пакет для работы с текстом, в нашем случае мы будем его использовать для формирования сообщения о погоде в выбранном городе.

Код 4: Бот, который сообщает текущую погоду по выбранному городу
library(telegram.bot)library(httr)library(stringr)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# создаём методы## метод для запуска основной клавиатурыstart <- function(bot, update) {  # создаём клавиатуру  RKM <- ReplyKeyboardMarkup(    keyboard = list(      list(        KeyboardButton("Погода")      )    ),    resize_keyboard = TRUE,    one_time_keyboard = TRUE  )  # отправляем клавиатуру  bot$sendMessage(update$message$chat_id,                  text = 'Выберите команду',                   reply_markup = RKM)}## Метод вызова Inine клавиатурыweather <- function(bot, update) {  IKM <- InlineKeyboardMarkup(    inline_keyboard = list(      list(        InlineKeyboardButton(text = 'Москва', callback_data = 'New York,us'),        InlineKeyboardButton(text = 'Санкт-Петербург', callback_data = 'Saint Petersburg'),        InlineKeyboardButton(text = 'Нью-Йорк', callback_data = 'New York')      ),      list(        InlineKeyboardButton(text = 'Екатеринбург', callback_data = 'Yekaterinburg,ru'),        InlineKeyboardButton(text = 'Берлин', callback_data = 'Berlin,de'),        InlineKeyboardButton(text = 'Париж', callback_data = 'Paris,fr')      ),      list(        InlineKeyboardButton(text = 'Рим', callback_data = 'Rome,it'),        InlineKeyboardButton(text = 'Одесса', callback_data = 'Odessa,ua'),        InlineKeyboardButton(text = 'Киев', callback_data = 'Kyiv,fr')      ),      list(        InlineKeyboardButton(text = 'Токио', callback_data = 'Tokyo'),        InlineKeyboardButton(text = 'Амстердам', callback_data = 'Amsterdam,nl'),        InlineKeyboardButton(text = 'Вашингтон', callback_data = 'Washington,us')      )    )  )  # Send Inline Keyboard  bot$sendMessage(chat_id = update$message$chat_id,                   text = "Выберите город",                   reply_markup = IKM)}# метод для сообщения погодыanswer_cb <- function(bot, update) {  # получаем из сообщения город  city <- update$callback_query$data  # отправляем запрос  ans <- GET('https://api.openweathermap.org/data/2.5/weather',              query = list(q     = city,                          lang  = 'ru',                          units = 'metric',                          appid = '4776568ccea136ffe4cda9f1969af340'))   # парсим ответ  result <- content(ans)  # формируем сообщение  msg <- str_glue("{result$name} погода:\n",                  "Текущая температура: {result$main$temp}\n",                  "Скорость ветра: {result$wind$speed}\n",                  "Описание: {result$weather[[1]]$description}")  # отправляем информацию о погоде  bot$sendMessage(chat_id = update$from_chat_id(),                  text    = msg)  bot$answerCallbackQuery(callback_query_id = update$callback_query$id) }# создаём фильтры## сообщения с текстом ПогодаMessageFilters$weather <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Погода"})# создаём обработчикиh_start         <- CommandHandler('start', start)h_weather       <- MessageHandler(weather, filters = MessageFilters$weather)h_query_handler <- CallbackQueryHandler(answer_cb)# добавляем обработчики в диспетчерupdater <- updater +               h_start +              h_weather +              h_query_handler# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

В результате наш бот будет работать примерно так:


Схематически данного бота можно изобрать вот так:


Мы создали 3 метода, доступные внутри нашего погодного бота:


  • start Запуск основной клавиатуры бота
  • weather Запуск Inline клавиатуры для выбора города
  • answer_cb Основной метод, который по заданному городу запрашивает в API погоду, и отправляет её в чат.

Метод start у нас запускается командой /start, что реализовано обработчиком CommandHandler('start', start).


Для запуска метода weather мы создали одноимённый фильтр:


# создаём фильтры## сообщения с текстом ПогодаMessageFilters$weather <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Погода"})

И вызываем этот метод следующим обработчиком сообщений: MessageHandler(weather, filters = MessageFilters$weather).


И в конце концов, основной наш метод answer_cb реагирует на нажатие Inline кнопок, что реализовано специальным обработчиком: CallbackQueryHandler(answer_cb).


Внутри метода answer_cb, мы считываем отправленные с клавиатуры данные и записываем их в переменную city: city <- update$callback_query$data. После чего запрашиваем из API данные о погоде, формируем и отправляем сообщение, и в конце концов используем метод answerCallbackQuery для того, что бы сообщить боту, о том, что мы обработали нажатие Inline кнопки.


Пример бота, который выводит список самых свежих статей со ссылками по-указанному Хабу из habr.com.


Данного бота я привожу для того, что бы показать вам, как вывести Inline кнопки которые ведут на веб страницы.


Логика данного бота схожа с предыдущим, изначально мы запускаем основную клавиатуру командой /start. Далее бот даёт нам на выбор список из 6 хабов, мы выбираем интересующий нас хаб, и получаем 5 самых свежих публикаций из выбранного Хаба.


Как вы понимаете, в данном случае нам необходимо получить список статей, и для этого мы будем использовать специальный пакет habR, который позволяет запрашивать из хабры статьи и некоторую статистику по ним в R.


Установить пакет habR можно только из github, для чего вам понадобится дополнительный пакет devtools. Для установки воспользуйтесь приведённым ниже кодом.


install.packages('devtools')devtools::install_github('selesnow/habR')

Теперь рассмотрим код построения описанного выше бота:


Код 5: Бот который выводит список наиболее свежих статей по выбранному Хабу
library(telegram.bot)library(habR)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# создаём методы## метод для запуска основной клавиатурыstart <- function(bot, update) {  # создаём клавиатуру  RKM <- ReplyKeyboardMarkup(    keyboard = list(      list(        KeyboardButton("Список статей")      )    ),    resize_keyboard = TRUE,    one_time_keyboard = TRUE  )  # отправляем клавиатуру  bot$sendMessage(update$message$chat_id,                  text = 'Выберите команду',                   reply_markup = RKM)}## Метод вызова Inine клавиатурыhabs <- function(bot, update) {  IKM <- InlineKeyboardMarkup(    inline_keyboard = list(      list(        InlineKeyboardButton(text = 'R', callback_data = 'R'),        InlineKeyboardButton(text = 'Data Mining', callback_data = 'data_mining'),        InlineKeyboardButton(text = 'Data Engineering', callback_data = 'data_engineering')      ),      list(        InlineKeyboardButton(text = 'Big Data', callback_data = 'bigdata'),        InlineKeyboardButton(text = 'Python', callback_data = 'python'),        InlineKeyboardButton(text = 'Визуализация данных', callback_data = 'data_visualization')      )    )  )  # Send Inline Keyboard  bot$sendMessage(chat_id = update$message$chat_id,                   text = "Выберите Хаб",                   reply_markup = IKM)}# метод для сообщения погодыanswer_cb <- function(bot, update) {  # получаем из сообщения город  hub <- update$callback_query$data  # сообщение о том, что данные по кнопке получены  bot$answerCallbackQuery(callback_query_id = update$callback_query$id,                           text = 'Подождите несколько минут, запрос обрабатывается')   # сообщение о том, что надо подождать пока бот получит данные  mid <- bot$sendMessage(chat_id = update$from_chat_id(),                         text    = "Подождите несколько минут пока, я соберу данные по выбранному Хабу")  # парсим Хабр  posts <- head(habr_hub_posts(hub, 1), 5)  # удаляем сообщение о том, что надо подождать  bot$deleteMessage(update$from_chat_id(), mid$message_id)   # формируем список кнопок  keys <- lapply(1:5, function(x) list(InlineKeyboardButton(posts$title[x], url = posts$link[x])))  # формируем клавиатуру  IKM <- InlineKeyboardMarkup(    inline_keyboard =  keys     )  # отправляем информацию о погоде  bot$sendMessage(chat_id = update$from_chat_id(),                  text    = paste0("5 наиболее свежих статей из Хаба ", hub),                  reply_markup = IKM)}# создаём фильтры## сообщения с текстом ПогодаMessageFilters$hubs <- BaseFilter(function(message) {  # проверяем текст сообщения  message$text == "Список статей"})# создаём обработчикиh_start         <- CommandHandler('start', start)h_hubs          <- MessageHandler(habs, filters = MessageFilters$hubs)h_query_handler <- CallbackQueryHandler(answer_cb)# добавляем обработчики в диспетчерupdater <- updater +   h_start +  h_hubs  +  h_query_handler# запускаем ботаupdater$start_polling()

Запустите приведённый выше пример кода, предварительно заменив 'ТОКЕН ВАШЕГО БОТА' на реальный токен, который вы получили при создании бота через BotFather (о создании бота я рассказывал в первой статье).

В итоге мы получим вот такой результат:


Список доступных для выбора Хабов мы вбили хардкодом, в методе habs:


## Метод вызова Inine клавиатурыhabs <- function(bot, update) {  IKM <- InlineKeyboardMarkup(    inline_keyboard = list(      list(        InlineKeyboardButton(text = 'R', callback_data = 'r'),        InlineKeyboardButton(text = 'Data Mining', callback_data = 'data_mining'),        InlineKeyboardButton(text = 'Data Engineering', callback_data = 'data_engineering')      ),      list(        InlineKeyboardButton(text = 'Big Data', callback_data = 'bigdata'),        InlineKeyboardButton(text = 'Python', callback_data = 'python'),        InlineKeyboardButton(text = 'Визуализация данных', callback_data = 'data_visualization')      )    )  )  # Send Inline Keyboard  bot$sendMessage(chat_id = update$message$chat_id,                   text = "Выберите Хаб",                   reply_markup = IKM)}

Список статей из указанного Хаба мы получаем командой habr_hub_posts(), из пакета habR. При этом указываем, что нам не требуется список статей за всё время, а только первая страница на которой располагаются 20 статей. Из полученной таблицы с помощью команды head() оставляем только 5 самых верхних, которые и являются самыми свежими статьями.


  # парсим Хабр  posts <- head(habr_hub_posts(hub, 1), 5)

Логика очень схожа с предыдущим ботом, но в данном случае Inline клавиатуру со списком статей мы генерируем динамически с помощью функции lapply().


  # формируем список кнопок  keys <- lapply(1:5, function(x) list(InlineKeyboardButton(posts$title[x], url = posts$link[x])))  # формируем клавиатуру  IKM <- InlineKeyboardMarkup(    inline_keyboard =  keys     )

В текст кнопки мы подставляем название статьи posts$title[x], а в аргумент url ссылку на статью: url = posts$link[x].


Далее, создаём фильтр, обработчики и запускаем нашего бота.


Заключение


Теперь написанные вами боты будут значительно удобней в работе, за счёт того, что управление ими будет осуществляться с клавиатуры, а не вводом команд. Как минимум при взаимодействии с ботом через смартфон клавиатура ощутимо упростит процесс его использования.


В следующей статье мы разберёмся как строить логический диалог с ботом, и работать с базами данных.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru