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

Telegram bot

Из песочницы 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 Bot и HomeKit

07.05.2021 14:19:42 | Автор: admin

Как я реализовал удаленное управление и мониторинг, для программируемого реле ПР200, используя разные сервисы (Telegram Bot, HomeKit) и протоколы (Modbus RTU, Modbus TCP, mqtt) и ESP32.

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

Долгое время я использовал сетевой интерфейс на основе модуля esp8266, на сегодня более перспективным вижу использование модуля esp32, долгое время я не рассматривал его из-за размеров, но впоследствии, проработав разные варианты, удалось не только вместить все на одной плате, но и сделать решение более универсальным и удобным.

Первая версия платы на основе ESP32

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

В обновленном варианте добавил ещё и кнопки сброса и загрузки при прошивке, а так же добавил поддержку модулей ESP32-WROVER с PSRAM, это позволит использовать больше памяти и расширит возможности.

В общем, структура взаимодействия сетевой платы с программируемым реле основана на протоколе modbus rtu, а с внешним миром варианты могут быть самые разнообразные от bluetooth до TelegramBot.

TelegramBot

Именно с поддержки бота я и начал опыты, на раннем этапе получилось вот такая реализация:

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

Для универсальности взаимодействие бота с алгоритмом в приборе, использован режим чтения/записи сетевых регистров Modbus а разных форматах представления:

/R- целое 16 битное значение

/I-целое число занимающее 2 регистра

/F- число в формате float тоже 2 регистра.

После символа адрес в диапазоне 512-576, эти регистры можно читать и записывать, формат для записи /Xzzz=nnnn, для чтения достаточно отправить номер регистра в требуемом формате.

Для представления состояний регистра в битовых полях, можно отправить адрес в формате /Bzzz, ответ будет в виде 16 значения в булевом формате.

Apple HomeKit

Следующим этапом был сервис Apple HomeKit и приложение дом, как раз для управления освещением и другими точечными нагрузками он подходит лучше всего, я начал с 16 каналов, по количеству бит в регистре модбас.

После выхода обзора по такому применению,

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

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

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

Так же протестировал mqtt, идея задания топиков взята из версии платы для esp8266. Проверил поддержку датчиков 1-wire ds18b20, для их подключения к плате предусмотрены посадочные места под разъем, и сигнальные линии с резисторами, такой-же использовался в плате prsd на esp8266.

4 пина, два из которых +3.3v и gnd, позволяют задействовать 2 порта в качестве интерфейса 1-wire или i2c. I2C позволяет подключать всякую экзотику, которую практически невозможно состыковать в базовой поставке прибора. Например, датчик влажности/давления с I2C или RFID ридер.

Для быстрого просмотра значений регистров используется протокол Modbus TCP, запустив Modbus Poll на ПК или Virtuino/Kascada и другие приложения на Android, можно быстро организовать доступ и управление устройством с помощью телефона или планшета.

Остальные настройки WEB интерфейса представлены ниже:

WEB настройки

Для смены прошивки платы, когда она уже установлена в устройство, предусмотрен режим обновления по воздуху (OTA), для этого достаточно выбрать bin файл, после загрузки прошивки устройство перезагрузится и запустится обновленная версия. Так же можно перезагрузить плату в ручном режиме через web кнопку.

При первом старте, когда устройство не имеет настроек точки доступа и пароля и не может подключиться к сети wi-fi, плата включает режим точки доступа для подключения и ввода ssid и pass, после сохранения значений и перезагрузки если подключение к сети успешно, точка доступа выключается. Если токен Telegram bot введен, то после подключения и выхода в интернет, узнать IP адрес платы можно введя команду. Через бот можно получить и другую информацию.

Основные моменты по работе представлены в видео.

На данный момент прошивка находится в режиме доработки, демонстрационные версии будут доступны позже.

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

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

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

Используя несложный сетевой интерфейс с чипом ESP32, можно значительно расширить функционал программируемого реле ПР200 и в перспективе ПР103, куда можно установить сетевой интерфейс, другие модели ПР100/ПР102 потребуют внешний драйвер RS-485 для подключения снаружи, так как сетевые интерфейсы в них не съемные.

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

Подробнее..

Практическое применение аннотации в Java на примере создания Telegram-бота

17.12.2020 14:07:58 | Автор: admin
Рефлексия в Java это специальное API из стандартной библиотеки, которая позволяет получить доступ к информации о программе во время выполнения.

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

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

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




Рефлексия



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

Reflection vs Introspection


Наряду с рефлексией также есть понятие интроспекции. Интроспекция это способность программы получить данные о типе и других свойствах объекта. Например, это instanceof:

if (obj instanceof Cat) {   Cat cat = (Cat) obj;   cat.meow();}

Это очень сильный метод, без чего Java не была бы такой, какая она есть. Тем не менее дальше получения данных он не уходит, и в дело вступает рефлексия.

Некоторые возможности рефлексии


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

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

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

Object obj = new Cat();    // а куда кошка пропала?

Воспользуемся рефлексией и создадим экземпляр класса:

Object obj = Class.forName("complete.classpath.MyCat").newInstance();

Давайте также через рефлексию вызовем его метод:

Method m = obj.getClass().getDeclaredMethod("meow");m.invoke(obj);

От теории к практике:

import java.lang.reflect.Method;import java.lang.Class;public class Cat {    public void meow() {        System.out.println("Meow");    }        public static void main(String[] args) throws Exception {        Object obj = Class.forName("Cat").newInstance();         Method m = obj.getClass().getDeclaredMethod("meow");         m.invoke(obj);    }}

Поиграть с ним можно в Jdoodle.
Несмотря на простоту, в этом коде происходит довольно много сложных вещей, и зачастую программисту не хватает лишь просто использования getDeclaredMethod and then invoke.

Вопрос #1
Почему в invoke методе в примере сверху мы должны передавать экземпляр объекта?

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

Аннотации


Важной частью языка Java являются аннотации. Это некоторый дескриптор, который можно повесить на класс, поле или метод. Например, вы могли видеть аннотацию @Override:

public abstract class Animal {    abstract void doSomething();}public class Cat extends Animal {    @Override    public void doSomething() {        System.out.println("Meow");    }}

Задумывались ли вы, как оно работает? Если не знаете, то, прежде чем читать дальше, попробуйте догадаться.

Типы аннотаций


Рассмотрим вышеприведённую аннотацию:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}

@Target указывает к чему применима аннотация. В данном случае, к методу.

@Retention длительность жизни аннотации в коде (не в секундах, разумеется).

@interface является синтаксисом для создания аннотаций.

Если с первым и последним все более менее понятно (подробнее см.@Targetвдокументации), то@Retentionдавайте разберем сейчас, так как он поможет разделить аннотации на несколько типов, что очень важно понимать.

Эта аннотация может принимать три значения:


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

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

В третьем случае аннотация будет удалена компилятором (её не будет в байт-коде). Обычно это бывают аннотации, которые полезны только для компилятора.

Возвращаясь к аннотации@Override, мы видим, что она имеетRetentionPolicy.SOURCEчто в общем-то логично, учитывая, что он используется только компилятором. В рантайме эта аннотация действительно ничего полезного не дает.

SuperCat


Попробуем добавить свою аннотацию (это здорово нам пригодится во время разработки).

abstract class Cat {    abstract void meow();}public class Home {    private class Tom extends Cat {        @Override        void meow() {            System.out.println("Tom-style meow!"); // <---        }    }        private class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!"); // <---        }    }}

Пусть у нас в доме будет два котика: Том и Алекс. Создадим аннотацию для суперкотика:

@Target(ElementType.TYPE)     // чтобы использовать для класса@Retention(RetentionPolicy.RUNTIME)  // хотим чтобы наша аннотация дожила до рантайма@interface SuperCat {}// ...    @SuperCat   // <---    private class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!");        }    }// ...

При этом Тома мы оставим обычным котом (мир несправедлив). Теперь попробуем получить классы, которые были аннотированы данным элементом. Было бы неплохо иметь такой метод у самого класса аннотации:

Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();

Но, к сожалению, такого пока метода нет. Тогда как нам найти эти классы?

ClassPath


Это параметр, который указывает на пользовательские классы.

Надеюсь, вы с ними знакомы, а если нет, то спешите изучить это, так как это одна из фундаментальных вещей.

Итак, узнав, где хранятся наши классы, мы сможем их загрузить через ClassLoader и проверить классы на наличие данной аннотации. Сразу приступим к коду:

public static void main(String[] args) throws ClassNotFoundException {    String packageName = "com.apploidxxx.examples";    ClassLoader classLoader = Home.class.getClassLoader();        String packagePath = packageName.replace('.', '/');    URL urls = classLoader.getResource(packagePath);        File folder = new File(urls.getPath());    File[] classes = folder.listFiles();        for (File aClass : classes) {        int index = aClass.getName().indexOf(".");        String className = aClass.getName().substring(0, index);        String classNamePath = packageName + "." + className;        Class<?> repoClass = Class.forName(classNamePath);            Annotation[] annotations = repoClass.getAnnotations();        for (Annotation annotation : annotations) {            if (annotation.annotationType() == SuperCat.class) {                System.out.println(                  "Detected SuperCat!!! It is " + repoClass.getName()                );            }        }        }}

Не советую использовать это в вашей программе. Код приведён только для ознакомительных целей!

Этот пример показателен, но используется только для учебных целей из-за этого:

Class<?> repoClass = Class.forName(classNamePath);

Дальше мы узнаем, почему. А пока разберём по строчкам весь код сверху:

// ...// пакет в котором мы сейчас находимсяString packageName = "com.apploidxxx.examples";// Загрузчик классов, чтобы получить наши классы из байт-кодаClassLoader classLoader = Home.class.getClassLoader();// com.apploidxxx.examples -> com/apploidxxx/examplesString packagePath = packageName.replace('.', '/');URL urls = classLoader.getResource(packagePath);File folder = new File(urls.getPath());// Наши классы в виде файловFile[] classes = folder.listFiles();// ...

Чтобы разобраться, откуда мы берём эти файлы, рассмотрим JAR-архив, который создаётся, когда мы запускаем приложение:

com   apploidxxx       examples               Cat.class               Home$Alex.class               Home$Tom.class               Home.class               Main.class               SuperCat.class

Таким образом, classes это только наши скомпилированные файлы в виде байт-кода. Тем не менее File это ещё не загруженный файл, мы только знаем, где они находятся, но мы пока по-прежнему не можем видеть, что находится внутри них.

Поэтому загрузим каждый файл:

for (File aClass : classes) {    // имя файла, на самом деле, Home.class, Home$Alex.class и тд    // поэтому нам нужно избавиться от .class и получить путь к файлу    // как к объекту внутри Java    int index = aClass.getName().indexOf(".");    String className = aClass.getName().substring(0, index);    String classNamePath = packageName + "." + className;    // classNamePath = com.apploidxxx.examples.Home    Class<?> repoClass = Class.forName(classNamePath);}

Всё, что сделано ранее, было только для того, чтобы вызвать этот метод Class.forName, который загрузит необходимый нам класс. Итак, финальная часть это получение всех аннотаций, использованных на класс repoClass, а затем проверка, являются ли они аннотацией @SuperCat:

Annotation[] annotations = repoClass.getAnnotations();for (Annotation annotation : annotations) {    if (annotation.annotationType() == SuperCat.class) {        System.out.println(          "Detected SuperCat!!! It is " + repoClass.getName()        );    }}output: Detected SuperCat!!! It is com.apploidxxx.examples.Home$Alex

И готово! Теперь, когда у нас есть сам класс, то мы получаем доступ ко всем методам рефлексии.

Рефлексируем


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

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

List<cat> superCats = new ArrayList<>();final Home home = new Home();    // дом, где будут жить наши котики

Итак, обработка обретает финальную форму:

for (Annotation annotation : annotations) {    if (annotation.annotationType() == SuperCat.class) {        Object obj = repoClass          .getDeclaredConstructor(Home.class)          .newInstance(home);        superCats.add((Cat) obj);    }}

И снова рубрика вопросов:

Вопрос #2
Что будет, если мы пометим @SuperCat класс, который не наследуется от Cat?

Вопрос #3
Почему нам нужен конструктор, который принимает тип аргумента Home?

Подумайте пару минут, а затем сразу разберём ответы:

Ответ #2: Будет ClassCastException, так как сама аннотация @SuperCat не гарантирует того, что класс, помеченный этой аннотацией, наследует что-то или имплементирует.

Вы можете проверить это, убрав extends Cat у Alex. Заодно вы убедитесь в том, насколько полезной может быть аннотация @Override.

Ответ #3: Кошкам нужен дом, потому что они являются внутренними классами. Всё в рамках спецификации The Java Language Specification глава 15.9.3.

Тем не менее вы можете избежать этого, просто сделав эти классы статическими. Но при работе с рефлексией вы часто будете сталкиваться с такого рода вещами. И вам на самом деле не нужно для этого досконально знать спецификацию Java. Эти вещи достаточно логичны, и можно додуматься самому, почему мы должны передавать в конструктор экземпляр родительского класса, если он non-static.

Подведём итоги и получим: Home.java

package com.apploidxxx.examples;import java.io.File;import java.lang.annotation.*;import java.lang.reflect.InvocationTargetException;import java.net.URL;import java.util.ArrayList;import java.util.List;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@interface SuperCat {}abstract class Cat {    abstract void meow();}public class Home {    public class Tom extends Cat {        @Override        void meow() {            System.out.println("Tom-style meow!");        }    }        @SuperCat    public class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!");        }    }        public static void main(String[] args) throws Exception {            String packageName = "com.apploidxxx.examples";        ClassLoader classLoader = Home.class.getClassLoader();            String packagePath = packageName.replace('.', '/');        URL urls = classLoader.getResource(packagePath);            File folder = new File(urls.getPath());        File[] classes = folder.listFiles();            List<Cat> superCats = new ArrayList<>();        final Home home = new Home();            for (File aClass : classes) {            int index = aClass.getName().indexOf(".");            String className = aClass.getName().substring(0, index);            String classNamePath = packageName + "." + className;            Class<?> repoClass = Class.forName(classNamePath);            Annotation[] annotations = repoClass.getAnnotations();            for (Annotation annotation : annotations) {                if (annotation.annotationType() == SuperCat.class) {                    Object obj = repoClass                      .getDeclaredConstructor(Home.class)                      .newInstance(home);                    superCats.add((Cat) obj);                }            }        }            superCats.forEach(Cat::meow);    }}output: Alex-style meow!

Так что не так с Class.forName?

Сам он как раз-таки делает всё, что от него нужно. Тем не менее мы его используем неправильно.

Представьте себе, что вы работаете над проектов в котором 1000 и больше классов (всё-таки на Java пишем). И представьте, что вы будете загружать каждый класс, который найдёте в classPath. Сами понимаете, что память и остальные ресурсы JVM не резиновые.

Способы работы с аннотациями


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

Но всё же Spring вроде работает. Неужели из-за них моя программа такая медленная? К сожалению или к счастью, нет. Spring работает исправно (в этом плане), потому что использует несколько иной способ для работы с ними.

Прямо в байт-код


Все (надеюсь) так или иначе имеют представление, что такое байт-код. В нём хранится вся информация о наших классах и их метаданных (в том числе аннотаций).

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

Так почему бы нам её просто не прочитать (да, из байт-кода)? Но здесь я не буду реализовывать программу для её чтения из байт-кода, так как это заслуживает отдельной статьи. Впрочем, вы сами можете это сделать это будет отличной практикой, которая закрепит материал статьи.

Для ознакомления с байт-кодом вы можете начать с моей статьи. Там я описываю базовые вещи байт-кода с программой Hello World! Статья будет полезна, даже если вы не собираетесь напрямую работать с байт-кодом. В нем описываются фундаментальные моменты, которые помогут ответить на вопрос: почему именно так?

После этого добро пожаловать на официальную спецификацию JVM. Если вы не хотите разбирать байт-код вручную (по байтам), то посмотрите в сторону таких библиотек, как ASM и Javassist.

Reflections


Reflections библиотека с WTFPL лицензией, которая позволяет делать с ней всё, что вы захотите. Довольно быстрая библиотека для различной работы с classpath и метаданными. Полезным является то, что она может сохранять информацию о уже некоторых прочитанных данных, что позволяет сэкономить время. Можете покопаться внутри и найти класс Store.

package com.apploidxxx.examples;import org.reflections.Reflections;import java.lang.reflect.InvocationTargetException;import java.util.Optional;import java.util.Set;public class ExampleReflections {    private static final Home HOME = new Home();    public static void main(String[] args) {            Reflections reflections = new Reflections("com.apploidxxx.examples");            Set<Class<?>> superCats = reflections          .getTypesAnnotatedWith(SuperCat.class);            for (Class<?> clazz : superCats) {            toCat(clazz).ifPresent(Cat::meow);        }    }        private static Optional<Cat> toCat(Class<?> clazz) {        try {            return Optional.of((Cat) clazz                               .getDeclaredConstructor(Home.class)                               .newInstance(HOME)                              );        } catch (InstantiationException |                  IllegalAccessException |                  InvocationTargetException |                  NoSuchMethodException e)         {            e.printStackTrace();            return Optional.empty();        }    }}

spring-context


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

Тем не менее существует множество других библиотек, работающих схожим образом. Их достаточно много, но сейчас я хочу разобрать лишь один из них это spring-context. Он, может быть, лучше первого, когда вы разрабатываете бота в среде Spring. Но и здесь есть пара нюансов.

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

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

Опять же, довольно редко вы должны будете использовать именно такой метод, но как вариант его стоит рассмотреть.
Я писал на нём бота для ВК. Вот репозиторий, с которым вы можете ознакомиться, но писал я его давно, а когда зашёл посмотреть, чтобы вставить ссылку в статью, то увидел, что через VK-Java-SDK я получаю сообщения с неинициализированными полями, хотя раньше всё работало.

Самое забавное, что я даже версию SDK не менял, поэтому, если вы найдёте, в чем была причина, буду благодарен. Тем не менее загрузка самих команд работает нормально, и это именно то, на что вы можете посмотреть, если хотите увидеть пример работы со spring-context.

Команды в нём представляют из себя вот что:

@Command(value = "hello", aliases = {"привет", "йоу"})public class Hello implements Executable {    public BotResponse execute(Message message) throws Exception {        return BotResponseFactoryUtil.createResponse("hello-hello",                                                      message.peerId);    }}

Примеры кода с аннотацией SuperCat вы можете найти в этом репозитории.

Практическое применение аннотаций в создании Телеграм-бота


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

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

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

Мы будем использовать библиотеку TelegramBots с MIT лицензией для работы с API телеграма. Вы же можете использовать любую другую. Я выбрал её, потому что она могла работать как c (имеет версию со стартёром), так и без спринг-бута.

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

Reflections


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

Во всех примерах будем придерживаться того, что бот состоит из нескольких команд, причём эти команды мы не будем загружать вручную, а просто будем добавлять аннотации. Вот пример команды:
@Handler("/hello")public class HelloHandler implements RequestHandler {    private static final Logger log = LoggerFactory      .getLogger(HelloHandler.class);        @Override    public SendMessage execute(Message message) {        log.info("Executing message from : " + message.getText());        return SendMessage.builder()                .text("Yaks")                .chatId(String.valueOf(message.getChatId()))                .build();    }}@Retention(RetentionPolicy.RUNTIME)public @interface Handler {    String value();}

В этом случае параметр /hello будет записан в value в аннотации. value это что-то вроде аннотации по умолчанию. То есть @Handler("/hello") = @Handler(value = "/hello").

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

@Retention(RetentionPolicy.RUNTIME)public @interface Log {    String value() default ".*";    // regex    ExecutionTime[] executionTime() default ExecutionTime.BEFORE;}default` означает, что значение будет применено, если не будет указан `value@Logpublic class LogHandler implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(LogHandler.class);        @Override    public void execute(Message message) {        log.info("Just log a received message : " + message.getText());    }}

Но также мы можем добавить параметр, чтобы логгер срабатывал при определённых сообщениях:

@Log(value = "/hello")public class HelloLogHandler implements RequestLogger {    public static final Logger log = LoggerFactory      .getLogger(HelloLogHandler.class);    @Override    public void execute(Message message) {        log.info("Received special hello command!");    }}

Или срабатывал после обработки запроса:

@Log(executionTime = ExecutionTime.AFTER)public class AfterLogHandler implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(AfterLogHandler.class);        @Override    public void executeAfter(Message message, SendMessage sendMessage) {        log.info("Bot response >> " + sendMessage.getText());    }}

Или и там, и там:

@Log(executionTime = {ExecutionTime.AFTER, ExecutionTime.BEFORE})public class AfterAndBeforeLogger implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(AfterAndBeforeLogger.class);    @Override    public void execute(Message message) {        log.info("Before execute");    }        @Override    public void executeAfter(Message message, SendMessage sendMessage) {        log.info("After execute");    }}

Мы можем делать такое, так как executionTime принимает массив значений. Принцип работы прост, поэтому приступим к обработке этих аннотаций:

Set<Class<?>> annotatedCommands =   reflections.getTypesAnnotatedWith(Handler.class);final Map<String, RequestHandler> commandsMap = new HashMap<>();final Class<RequestHandler> requiredInterface = RequestHandler.class;for (Class<?> clazz : annotatedCommands) {    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {        for (Constructor<?> c : clazz.getDeclaredConstructors()) {            //noinspection unchecked            Constructor<RequestHandler> castedConstructor =               (Constructor<RequestHandler>) c;            commandsMap.put(extractCommandName(clazz),                             OBJECT_CREATOR.instantiateClass(castedConstructor));        }    } else {        log.warn("Command didn't implemented: "                  + requiredInterface.getCanonicalName());        }}// ...private static String extractCommandName(Class<?> clazz) {    Handler handler = clazz.getAnnotation(Handler.class);    if (handler == null) {        throw new           IllegalArgumentException(            "Passed class without Handler annotation"            );    } else {        return handler.value();    }}

По сути, мы просто создаём мапу с именем команды, которую берём из значения value в аннотации. Исходный код здесь.

То же самое делаем с Log, только логгеров с одинаковыми паттернами может быть несколько, поэтому мы чуть меняем нашу структуру данных:

Set<Class<?>> annotatedLoggers = reflections.getTypesAnnotatedWith(Log.class);final Map<String, Set<RequestLogger>> commandsMap = new HashMap<>();final Class<RequestLogger> requiredInterface = RequestLogger.class;for (Class<?> clazz : annotatedLoggers) {    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {        for (Constructor<?> c : clazz.getDeclaredConstructors()) {            //noinspection unchecked            Constructor<RequestLogger> castedConstructor =               (Constructor<RequestLogger>) c;            String name = extractCommandName(clazz);            commandsMap.computeIfAbsent(name, n -> new HashSet<>());            commandsMap              .get(extractCommandName(clazz))              .add(OBJECT_CREATOR.instantiateClass(castedConstructor));        }        } else {        log.warn("Command didn't implemented: "                  + requiredInterface.getCanonicalName());    }}

На каждый паттерн приходится несколько логгеров. Остальное всё так же.
Теперь в самом боте нам нужно настроить executionTime и перенаправлять запросы на эти классы:

public final class CommandService {    private static final Map<String, RequestHandler> commandsMap       = new HashMap<>();    private static final Map<String, Set<RequestLogger>> loggersMap       = new HashMap<>();        private CommandService() {    }        public static synchronized void init() {        initCommands();        initLoggers();    }        private static void initCommands() {        commandsMap.putAll(CommandLoader.readCommands());    }        private static void initLoggers() {        loggersMap.putAll(LogLoader.loadLoggers());    }        public static RequestHandler serve(String message) {        for (Map.Entry<String, RequestHandler> entry : commandsMap.entrySet()) {            if (entry.getKey().equals(message)) {                return entry.getValue();            }        }            return msg -> SendMessage.builder()                .text("Команда не найдена")                .chatId(String.valueOf(msg.getChatId()))                .build();    }        public static Set<RequestLogger> findLoggers(      String message,       ExecutionTime executionTime    ) {        final Set<RequestLogger> matchedLoggers = new HashSet<>();        for (Map.Entry<String, Set<RequestLogger>> entry:loggersMap.entrySet()) {            for (RequestLogger logger : entry.getValue()) {                    if (containsExecutionTime(                  extractExecutionTimes(logger), executionTime                ))                 {                    if (message.matches(entry.getKey()))                        matchedLoggers.add(logger);                }            }            }            return matchedLoggers;    }        private static ExecutionTime[] extractExecutionTimes(RequestLogger logger) {        return logger.getClass().getAnnotation(Log.class).executionTime();    }        private static boolean containsExecutionTime(      ExecutionTime[] times,      ExecutionTime executionTime    ) {        for (ExecutionTime et : times) {            if (et == executionTime) return true;        }            return false;    }}public class DefaultBot extends TelegramLongPollingBot {    private static final Logger log = LoggerFactory.getLogger(DefaultBot.class);    public DefaultBot() {        CommandService.init();        log.info("Bot initialized!");    }        @Override    public String getBotUsername() {        return System.getenv("BOT_NAME");    }        @Override    public String getBotToken() {        return System.getenv("BOT_TOKEN");    }        @Override    public void onUpdateReceived(Update update) {        try {            Message message = update.getMessage();            if (message != null && message.hasText()) {                // run "before" loggers                CommandService                  .findLoggers(message.getText(), ExecutionTime.BEFORE)                  .forEach(logger -> logger.execute(message));                    // command execution                SendMessage response;                this.execute(response = CommandService                             .serve(message.getText())                             .execute(message));                    // run "after" loggers                CommandService                  .findLoggers(message.getText(), ExecutionTime.AFTER)                  .forEach(logger -> logger.executeAfter(message, response));                }        } catch (Exception e) {            e.printStackTrace();        }    }}

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

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

Во-вторых, сама библиотека TelegramBots, как мне кажется, не особо ориентирована на такую работу (архитектуру) бота. Если же вы будете разрабатывать бота именно на этой библиотеке, то можете использовать Ability Bot, который указан в wiki самой библиотеки. Но очень хочется увидеть полноценную библиотеку с такой архитектурой. Поэтому можете начать писать свою библиотеку!

Спринговый бот


Это приобретает больше смысла при работе с экосистемой спринга:

  • Работа через аннотации не нарушает общей концепции работы спринг-контейнера.
  • Мы можем не создавать команды сами, а получить их из контейнера, пометив наши команды как бины.
  • Получаем отличный DI от спринга.

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

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

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

Реализация


Что ж, приступим к самому боту.

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

Более самостоятельные разработчики могут сразу приступить к чтению кода.

Здесь же я разберу именно чтение команд, хотя в самом репозитории есть ещё пара интересных моментов, которые вы можете рассмотреть самостоятельно.
Реализация очень похожа на бота через Reflections, поэтому аннотации те же.

ObjectLoader.java

@Servicepublic class ObjectLoader {    private final ApplicationContext applicationContext;    public ObjectLoader(ApplicationContext applicationContext) {        this.applicationContext = applicationContext;    }        public Collection<Object> loadObjectsWithAnnotation(      Class<? extends Annotation> annotation    ) {        return applicationContext.getBeansWithAnnotation(annotation).values();    }}

CommandLoader.java

public Map<String, RequestHandler> readCommands() {    final Map<String, RequestHandler> commandsMap = new HashMap<>();        for (Object obj : objectLoader.loadObjectsWithAnnotation(Handler.class)) {        if (obj instanceof RequestHandler) {            RequestHandler handler = (RequestHandler) obj;            commandsMap.put(extractCommandName(handler.getClass()), handler);        }    }        return commandsMap;}

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

Подведём итоги


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

  • Reflections.
  • Spring-Context (без Spring).
  • ApplicationContext из Spring.

Тем не менее я могу дать вам совет, основываясь на своём опыте:

  1. Подумайте, нужен ли вам Spring. Он даёт мощный IoC контейнер и возможности экосистемы, но за всё приходится платить. Обычно я рассуждаю так: если нужны база данных и быстрый старт, то Spring Boot вам нужен. Если же бот достаточно прост, то можно обойтись и без него.
  2. Если же вам не нужны сложные зависимости, то смело используйте Reflections.

Реализация, например, JPA без Spring Data мне кажется довольно трудоёмкой задачей, хотя вы также можете посмотреть на альтернативы в виде micronaut или quarkus, но о них я только наслышан и не имею достаточного опыта, чтобы что-то советовать относительно этого.

Если вы приверженец более чистого подхода с нуля даже без JPA, то посмотрите на этого бота, который работает через JDBC через ВК и Телеграм.

Там вы увидите много записей вида:

PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");stmt.setString(1, aliases.toJSON());stmt.setInt(2, vkid);stmt.execute();

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

Также лично мне не особо нравится работать напрямую с Hibernate. Я уже имел печальный опыт писать DAO и HibernateSessionFactoryUtil (те, кто писал, поймут, о чём я).

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

Всем удачи! И не забывайте о промокоде HABR, который дает дополнительную скидку 10% к той, что указана на баннере.

image



Подробнее..

Keyboa клавиатуры на максималках для ботов в Telegram

09.10.2020 14:15:38 | Автор: admin

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

Keyboa помогает создавать встроенные клавиатуры любой сложности для ботов, разработанных на базе pyTelegramBotAPI.

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

Статья рассчитана на тех, кто знает основы Telegram Bot API и хотя бы немного знаком с фреймворком pyTelegramBotAPI.

Модуль требует > Python 3.5 и ставится через pip:

pip install keyboa

В официальной документации Telegram объект inline_keyboard определен как массив, состоящий из массивов кнопок (Array of Array of InlineKeyboardButton). То есть основной массив (список) - это клавиатура в целом, а его элементы (вложенные списки) - это ряды кнопок. Не переживайте, если определение кажется сложным - позже мы разберём его на простом примере.

Предположим, что сам бот у нас уже настроен:

import osfrom telebot import TeleBotfrom keyboa import keyboa_makertoken = os.environ["TELEGRAM_BOT_TOKEN"]uid = os.environ["TELEGRAM_USER_ID"] bot = TeleBot(token=token)

Создаём клавиатуру

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

fruits = [  "banana", "coconut", "orange",  "peach", "apricot", "apple",  "pineapple", "avocado", "melon"]

Создадим самую простую клавиатуру, в которой каждый элемент встанет на отдельный ряд:

kb_fruits = keyboa_maker(items=fruits, copy_text_to_callback=True)

Всё, что потребовалось - передать список в items. Оставить пустой сallback_data (ответную часть кнопки) мы не можем, ведь иначе не узнаем, что нажал пользователь. Поэтому добавляем текст каждой кнопки в её callback_data, устанавливая параметр copy_text_to_callback.

Отправим сообщение пользователю:

bot.send_message(  chat_id=uid, reply_markup=kb_fruits,  text="Please select one of the fruit:")

Распределяем кнопки по рядам

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

Указать количество кнопок в ряду

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

Создадим клавиатуру и отправим обновлённое сообщение:

kb_fruits = keyboa_maker(items=fruits, copy_text_to_callback=True, items_in_row=3)bot.send_message(  chat_id=uid, reply_markup=kb_fruits,  text="Please select one of the fruit:")

Использовать автоподбор

Если клавиатура создается динамически, мы не всегда знаем, сколько в ней окажется кнопок. Чтобы автоматически равномерно распределить их по рядам, установите параметр auto_alignment. Если выставить True, Keyboa попробует подобрать делитель в диапазоне от трёх до пяти, а можно задать свой диапазон в виде списка чисел от одного до восьми, например, [2, 3, 7].

Параметр reverse_alignment_range определяет, будет ли Keyboa искать делитель с начала или с конца диапазона, указанного в auto_alignment.

Распределить кнопки вручную

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

fruitscomplex = [  "banana",  ["coconut", "orange"],  ["peach", "apricot", "apple"],  "pineapple",  ["avocado", "melon"],]kb_fruits_complex = keyboa_maker(items=fruits_complex, copy_text_to_callback=True)bot.send_message(  chat_id=uid, reply_markup=kb_fruits_complex,  text="Please select one of the fruit:")

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

Управляем содержимым callback

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

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

fruits_with_ids = [  {"banana": "101"}, {"coconut": "102"}, {"orange": "103"},  {"peach": "104"}, {"apricot": "105"}, {"apple": "106"},  {"pineapple": "107"}, {"avocado": "108"}, {"melon": "109"}, ]# or [# ("banana", "101"), ("coconut", "102"), ("orange", "103"),# ("peach", "104"), ("apricot", "105"), ("apple", "106"),# ("pineapple", "107"), ("avocado", "108"), ("melon", "109"), ]

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

kb_fruits = keyboa_maker(items=fruits_with_ids, items_in_row=3)bot.send_message(  chat_id=uid, reply_markup=kb_fruits,text="Please select one of the fruit:")

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

{"text": "banana", "callback_data": "101"}, ...

Этого достаточно, если мы используем числа только для обозначения фруктовых id. А если нужно принимать разные числовые параметры, например вес или цену? Как определить, к чему относится полученное число?

С Keyboa мы можем явно указать это при создании клавиатуры. Параметр front_marker добавляет одинаковый текст в начало callback_data каждой кнопки, а back_marker, соответственно, в конец. Рассмотрим на примере:

kb_fruits = keyboa_maker(items=fruits_with_ids, front_marker="&fruit_id=")

Теперь callback_data кнопок будет состоять из текста формата front_marker + value, например: '&fruit_id=101', '&fruit_id=102' и т. д. Такой подход позволит нам легко расшифровать строку и понять, какие получены данные и с какими значениями.

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

# коллбек, полученный после нажатия на кнопку, например "&fruit_id=102"selected_fruit_id = call.dataavailable_weight = [1, 2, 5, 10, 100, ]kb_available_weight = keyboa_maker(items=available_weight, copy_text_to_callback=True, front_marker="&weight=", back_marker=selected_fruit_id)

Теперь callback_data каждой кнопки будет выглядеть так:

'&weight=1&fruit_id=102', '&weight=2&fruit_id=102', '&weight=5&fruit_id=102'

Здесь мы используем front_marker и back_marker, чтобы обернуть передаваемое значение, указав тип текущего значения и добавив в callback_data уже полученную информацию. Имейте в виду, что у Telegram есть ограничение на длину callback_data в 64 байта, поэтому, если передаваемых параметров будет много - придётся сокращать названия или использовать аббревиатуры.

Объединение нескольких клавиатур в одну

Иногда в одну клавиатуру могут входить статические и динамические элементы, например, навигационное меню и меняющийся набор элементов. Тогда возникает потребность собрать клавиатуру из нескольких частей. Специально для этого у нас есть функцияkeyboa_combiner().

У нее есть только один параметр - keyboards, представляющий собой набор уже созданных клавиатур. Вот, как это работает:

from keyboa import keyboa_maker, keyboa_combinercontrols = [["", "", "", "", ""], ]tracks = list(range(1, 13))keyboard_controls = keyboa_maker(items=controls, copy_text_to_callback=True)keyboard_tracks = keyboa_maker(items=tracks, items_in_row=4, copy_text_to_callback=True)keyboard = keyboa_combiner(keyboards=(keyboard_tracks, keyboard_controls))bot.send_message(  chat_id=uid, reply_markup=keyboard,   text="Please select the track number:")

Можно объединить сразу много клавиатур - главное, чтобы общее количество кнопок было не больше ста. Это ограничение Telegram.

Итог

В этой статье мы кратко рассмотрели создание клавиатур для ботов в Telegram с помощью Keyboa. Больше информации и описание остальных параметров можно найти на странице проекта в Github.

Буду рад вашим отзывам, идеям и конструктивной критике по развитию проекта.

Подробнее..

Подключение автоплатежей через TeleWalletAbot к своему Telegram-боту

20.10.2020 20:09:53 | Автор: admin
Доброго времени суток.
Сегодня хочу рассказать о том, как работать с платежным API не так давно появившегося на просторах Telegram кошелька TeleWallet Статья будет интересна в первую очередь владельцам и разработчикам ботов Telegram, поскольку эта платежная система позволяет принимать платежи в Телеграме, не покидая Telegram

Хочу сразу отметить, что я не выступаю за или против какой-либо платежной системы, а хочу лишь описать, как работать с API.

Преимущества и недостатки


Основные преимущества (на мой взгляд) приема платежей через TeleWallet:
  • Низкая комиссия (0.8% суммарно за автопополнение и автовыплату)
  • Отсутствие минимальной суммы (точнее, минимальная сумма составляет 0.01)
  • Доступность нескольких валют (фиатных и крипто)
  • Возможность подключения неограниченного количества проектов на один профиль


Основные недостатки:
  • Слабая распространенность ресурса (хотя это скорее всего исправимо)
  • Отсутствие английского интерфейса (кошелек рассчитан на русскоязычную аудиторию и соответственно распространенные в СНГ платежные системы)
  • Недостаточно автоматизированные переводы средств в сам кошелек (на данный момент доступно автоматическое пополнение кошелька только через Яндек.Деньги и Visa/MasterCard. Для других платежных систем доступно полуавтоматическое пополнение, из-за чего присутствует время ожидания)


Подключение автоплатежей


Итак приступим. Для начала набросаем небольшого тестового бота на PHP c 3-мя кнопками:
  • Баланс чтоб просматривать баланс и видеть, что он изменился
  • Пополнить
  • Вывести

Про регистрацию бота в BotFather рассказывать не буду: слишком уж много сказано до меня на эту тему. Для работы с Telegram-bot-api будем использовать irazasyed/telegram-bot-sdk. Как работать с этим SDK, и кстати как зарегистрировать бота в BotFather и установить webhook на него неплохо описано здесь.
И конечно для работы с TeleWallet API будем использовать их официальный SDK. Там же у них есть подробная инструкция, как работать с платежами и есть примеры кода. Так что ниже я просто покажу как совместить приведенные там примеры кода с реальным ботом.

Создаем платежный счёт



  1. Перейдите в бота t.me/TeleWalletAbot
  2. Запустите его
  3. Нажмите кнопку Прием платежей в главном меню
  4. Под появившимся сообщением нажмите кнопку Открыть платежный счёт
  5. Выберите валюту счёта
  6. Появится сообщение Счёт успешно создан

image
Шаги, начиная с 3-го, показаны на картинке. Только что добавленный счет вы увидите последним в списке ваших платежных счетов (7 на картинке). Нажмите на команду-ссылку напротив него, чтоб перейти к его настройкам (8 на картинке). Сообщение, которое мы получаем в ответ, выглядит вот так:
Настройка счёта ap110741100
Баланс: 0 RUB

Название магазина: Не задано
API ключ: eHW2IQZQYjlJjgQ
URL для уведомлений: Не задан
URL перехода после успешного платежа: Не задан
URL перехода после неудачи(отказа): Не задан
Плательщик комиссии при выплате: Магазин
Удалить счёт (/delapsch_100Re6)

С помощью кнопок под этим сообщением отредактируйте необходимые параметры


Создаем файл настроек


Создадим файл config.php и вставим туда следующий код
<?php  $dblocation = "localhost";  $dbname = "имя базы данных mysql";    $dbuser = "пользователь базы данных mysql";  $dbpasswd = "пароль к бд mysql";  /* Подключение к серверу MySQL */   $link = mysqli_connect($dblocation,$dbuser,$dbpasswd,$dbname);         if(!$link)  exit("<P>Невозможно установить соединение с сервером БД.</P>" );   mysqli_query($link,"SET NAMES 'utf8'");  //апи-ключ и счёт TeleWalletAbot  $tlwkey = "eHW2IQZQYjlJjgQ";  $tlwacc = "ap110741100";?>

Здесь мы подключим базу данных mysql (она нам понадобиться, чтоб хранить балансы пользователей и информацию о платежах) и заведем 2 переменные $tlwkey и $tlwacc для хранения API ключа и номера счёта из сообщения, полученного от бота.
В нашей базе данных создадим 2 таблицы:
1) users(id,name,balance,outnumber) будем хранить данные о пользователях
2) donate(id,user_id,sum,finished) информация о пополнениях

Далее создадим файл index.php (на него будем направлять наш webhook от Telegram) и вставим туда следующий код:
<?php      include('vendor/autoload.php');  //подключение библиотек, загруженных через composer    //классы для работы с Telegram bot api    use Telegram\Bot\Api;     use Telegram\Bot\Commands\Command;    use Telegram\Bot\Keyboard\Keyboard;    // -----------------------------------------    require_once "commands.php"; //скрипт, где мы реализуем основную логику    require_once "config.php"; //файл настроек    require_once "TeleWallet.php"; /*SDK Telewallet  https://github.com/tlwadmin/TeleWalletSDK/blob/main/TeleWallet.php  */    $telegram = new Api("токен, полученный от Telegram");    $result = $telegram -> getWebhookUpdates(); //получаем обновления    $chat_id = $result["message"]["chat"]["id"];    $text = $result["message"]["text"];    $callback_query = $result['callback_query'];        $data = $callback_query['data'];     $chat_id_in = $callback_query['message']['chat']['id'];     $uname = $result["message"]["from"]["username"]; if($chat_id>0 && $text){ //блок обработки основных команд$sm=['chat_id' => $chat_id, 'text' => $text];$ans_arr=getAnsw($text,$chat_id,$uname);    for($i=0;$i<count($ans_arr);$i++){$ans=$ans_arr[$i];$reply = $ans['text'];$sm=[ 'chat_id' => $chat_id, 'text' => $reply, 'caption'=>$reply];if(array_key_exists('inline_keyboard',$ans)) {$keyboard=$ans['inline_keyboard'];$replyMarkup = json_encode($keyboard);    $sm['reply_markup'] =$replyMarkup;}       else if(array_key_exists('keyboard',$ans)){$keyboard=$ans['keyboard'];$reply_markup = $telegram->replyKeyboardMarkup([ 'keyboard' => $keyboard, 'resize_keyboard' => true, 'one_time_keyboard' => false ]);$sm['reply_markup']=$reply_markup;}$telegram->sendMessage($sm);}            }    if($data){  //блок обработки инлайн-команд$ans_arr=getAnsw($data,$chat_id_in);for($i=0;$i<count($ans_arr);$i++){$ans=$ans_arr[$i];$reply = $ans['text'];$sm=[ 'chat_id' => $chat_id_in, 'text' => $reply, 'caption'=>$reply];if(array_key_exists('inline_keyboard',$ans)) {$keyboard=$ans['inline_keyboard'];$replyMarkup = json_encode($keyboard);    $sm['reply_markup'] =$replyMarkup;}       else if(array_key_exists('keyboard',$ans)){$keyboard=$ans['keyboard'];$reply_markup = $telegram->replyKeyboardMarkup([ 'keyboard' => $keyboard, 'resize_keyboard' => true, 'one_time_keyboard' => false ]);$sm['reply_markup']=$reply_markup;}$telegram->sendMessage($sm);}}    ?>


Здесь мы определяем, какое сообщение получено от пользователя. Отдаем его, а также идентификатор пользователя в функцию getAnsw(). Она возвращает массив сообщений, которые мы пересылаем пользователю.
Теперь создадим файл commands.php и вставим в него реализацию функции getAnsw()
<?phpfunction getAnsw($command,$chat_id, $name=""){global $link;global $telegram;global  $tlwkey;    global$tlwacc;$r=mysqli_query($link,"select * from users where id='$chat_id'");$ud=mysqli_fetch_assoc($r); //данные о пользователеif($command=="/start") {//добавим в бд если нетif(!$ud) mysqli_query($link,"INSERT INTO `users`(`id`,`name`) values('$chat_id','$name')");$res['text']="Привет. Я бот, позволяющий протестировать пополнение и выплату через TeleWallet";$res['keyboard']=[["Баланс","Пополнить","Выплата"]];return [$res];}if($command=="Баланс") {$res['text']="Ваш баланс: {$ud['balance']} руб";return [$res];}if($command=="Пополнить") {$res['text']="Выберите сумму, на которую хотите пополнить счёт";$res['inline_keyboard']['inline_keyboard']=[[['text'=>'1 руб','callback_data'=>'popoln_1'],['text'=>'5 руб','callback_data'=>'popoln_5'],['text'=>'10 руб','callback_data'=>'popoln_10']]];return [$res];}if($command=="Выплата") {$res['text']="Сколько вы хотите вывести?";$res['inline_keyboard']['inline_keyboard']=[[['text'=>'1 руб','callback_data'=>'vivod_1'],['text'=>'5 руб','callback_data'=>'vivod_5'],['text'=>'10 руб','callback_data'=>'vivod_10']]];addCmd("vivod_",$chat_id);return [$res];}$tlw = new TeleWallet($tlwkey,$tlwacc);if(strpos($command,'popoln_')!==false) {$arr = explode("_",$command);mysqli_query($link,"INSERT INTO `donate`( `user_id`, `sum`) values('$chat_id','{$arr[1]}')");$payId = mysqli_insert_id($link);$resp = $tlw->getСheque($arr[1],$payId);$res['text']="Вы выбрали пополнение на {$arr[1]} руб. Пополнение доступно через TeleWallet. Для продолжения нажмите кнопку под этим сообщением";$res['inline_keyboard']['inline_keyboard']=[[["text"=>"Пополнить","url"=>$resp['url']]]];return [$res];}       if(strpos($command,'setnumber_')!==false) { //пользователь задает номер счета$arr = explode("_",$command);mysqli_query($link,"UPDATE `users` SET `outnumber`='{$arr[1]}' where `id`='$chat_id'");$res['text']="Номер счёта обновлен.";return [$res];}if(strpos($command,'vivod_')!==false) {$arr = explode("_",$command);if($ud['balance']<$arr[1]) $res['text']="недостаточно средств на балансе";else {if(empty($ud['outnumber']))$res['text']="У вас не задан номер счета TeleWallet для вывода. Отправьте боту setnumber_(номер ваше счета) (без скобок), чтоб задать номер счёта";else {$resp = $tlw->sendOutpay($arr[1],$ud['outnumber']);if($resp['error']==0) { //выплата удалась$res['text']="На указанный Вами номер счета выведено {$arr[1]} руб";mysqli_query($link,"UPDATE `users` SET `balance`=`balance`-{$arr[1]} where `id`='$chat_id'");}else if($resp['error']==4 || $resp['error']==5) { //выплата удалась$res['text']="Вы указали неправильный номер счёта";}else $res['text']="Выплата не удалась. Код ошибки: {$resp['error']}. Обратитесь к администратору проекта";}}return [$res];}}?>

Когда пользователь нажимает Пополнить и выбирает сумму с помощью инлайн-кнопок под сообщением, создается платежная ссылка в строке
$resp = $tlw->getСheque($arr[1],$payId);

Функция getСheque вернет ассоциативный массив с параметрами error и url. error должно быть 0, и это желательно тоже проверять, но я для краткости опустил эту проверку. Параметр url мы используем, чтоб сформировать сообщение с инлайн-кнопкой, при нажатии на которую пользователь нашего бота попадет в @TeleWalletAbot и совершит оплату (или не совершит). Проверка факта оплаты описана ниже.
Когда пользователь заказывает вывод, у него должен быть задан номер счета для вывода. Если это не так, мы сообщаем ему об этом. Если счет задан, мы пытаемся выполнить вывод, используя функцию sendOutpay, и если возвращенный ею код ошибки 0 значит вывод прошел успешно, и мы списываем средства с баланса пользователя в нашем боте.

Проверка факта оплаты


Для проверки факта оплаты и зачисления средств на баланс пользователя создадим еще один скрипт: notice.php. Вот его код:
<?phpinclude('vendor/autoload.php'); use Telegram\Bot\Api; use Telegram\Bot\Commands\Command;use Telegram\Bot\Keyboard\Keyboard;    require_once "config.php";require_once "TeleWallet.php";$telegram = new Api("ключ апи вашего бота, полученный от ботфазер");$tlw = new TeleWallet($tlwkey,$tlwacc);$ri = mysqli_query($link,"SELECT * FROM `donate` WHERE `id`={$_POST['payId']}");$pay_info = mysqli_fetch_assoc($ri);if($tlw->testPayIn($_POST) && $pay_info['sum']==$_POST['sum']) {echo "YES";mysqli_query($link,"UPDATE `users` SET `balance`=`balance`+{$pay_info['sum']} where `id`={$pay_info['user_id']}");       mysqli_query($link,"UPDATE `users` SET `balance`=`balance`+{$pay_info['sum']} where `id`={$_POST['payId']}");try {$telegram->sendMessage(["text"=>"Ваш баланс пополнен на {$pay_info['sum']} руб","chat_id"=>$pay_info['user_id']]);}catch(Exception $e) {}}else echo "NO";?>

На этот файл будет прилетать вебхук от кошелька, когда пользователь успешно завершит оплату. Описание параметров POST-запроса смотрите в документации к SDK github.com/tlwadmin/TeleWalletSDK

Давайте вернемся теперь к нашему платежному счёту в кошельке. Нажмем кнопку URL для уведомлений и отправим боту ссылку на наш файл notice.php
Также укажите название магазина (точнее вашего проекта). В качестве URL успешно и URL fail просто укажите ссылку на ваш проект (бот)

Ну вот пожалуй и всё. Мы создали с вами тестового бота, который позволяет понять, как работать с платежным API TeleWallet

Посмотреть, как работает этот тестовый пример (там правда еще добавлен ручной ввод суммы и счёта при выводе) можно по ссылке: http://t.me/TlwSdkBot

Для лучшего понимания кода в статье, а также, чтоб узнать описание кодов ошибок и параметры запросов, смотрите документацию github.com/tlwadmin/TeleWalletSDK
Подробнее..

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-боты и зачем они нужны

05.08.2020 12:20:05 | Автор: admin
Telegram предоставляет возможность делать ботов быстро, удобно и подключать их к своим внутренним системам, создавая множество подвязок, триггеров и шаблонов. Активируешь Botfather-а и просто следуешь инструкциям, самое сложное обычно касается именно работы со внутренними системами и налаживанием API.



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

Почему Telegram


Telegram мессенджер, который есть у очень большого числа людей. И если какое-то время он был не очень популярным, то масштабная кампания по его блокировке (попыткам блокировки, да) стала отличным средством для поднятия интереса и дополнительных установок. Плюс, что выгодно отличает Telegram от тех же WhatsApp и Viber, это наличие простого и понятного API и возможностей подцепить к боту множество внутренних систем.

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

Бот для покупателей


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

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

image

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

Может показаться, что в ситуации с ботом проблем не будет просто читаешь справку по API, делаешь все по инструкции, Botfather в помощь, но на самом деле сложности бывают. У нас был ряд проблем, связанный с блокировкой API. Из-за сложности архитектуры некоторые пакеты терялись, сервер Telegram получал отказ на сетевом уровне и повторял запрос лишь через некоторое время. В итоге получали довольно медленный отклик бота, что раздражало покупателей. Потому что дико тормозящий бот в сознании пользователя мало чем отличается от отсутствия бота в принципе.

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

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

Сейчас покупательский бот не дорабатывается. Все новые функции, которые мы готовим, в том числе по просьбам покупателей, будут появляться только в мобильных приложениях (AppStore / Play Market). Их мы разработали для ВкусВилла на замену боту.

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

Бот доставки


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



Как мы писали выше, с помощью команды (1С-разработчик, SQL-разработчик и аналитик) на всё про всё ушло 5 дней. Сначала обсудили и зафиксировали необходимую функциональность, затем, собственно, написали бота, реализовали необходимые для его нормальной работы SQL-процедуры и развернули все базы. Это первые три дня. Четвертый день решили полностью посвятить активному тестированию, а на пятый день запустили бота на курьеров.

Итого вышло, что курьерского бота сделала команда из трёх человек за 5 дней. На поддержку и какие-то доработки в случае чего уходит не более часа работы в день.

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

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

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

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

Работает все довольно просто:

  1. Курьер отмечается в чате с ботом, что готов к работе.
  2. Получает от бота уведомление, что появился новый заказ, который надо доставить.
  3. Видит свободные для доставки заказы на карте, прикидывает, какие сейчас удобнее и быстрее всего доставить.
  4. Получает возможность построить маршрут в навигаторе для поездки, а также может связаться с покупателем по номеру телефона.
  5. Когда доставляет заказ покупателю (или возвращает в магазин по какой-то причине), то отписывается в чат боту. Бот меняет статус заказа.

Работает и активно дорабатывается. Как видите, тут получилась обратная ситуация: в случае с покупателями мы ушли от бота в сторону полноценного и функционального мобильного приложения под две платформы.

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

Другие внутренние боты


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

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

image

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

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

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

Пишем 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 (часть 5) Управление правами пользователей бота

01.10.2020 10:13:11 | Автор: admin

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


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



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


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

Содержание


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


  1. Введение
  2. Ограничиваем права пользователя с помощью фильтров сообщений
    2.1. Ограничиваем права на уровне имени пользователя
    2.2. Ограничиваем права на уровне чата
  3. Ограничиваем права пользователя внутри кода методов
    3.1. Ограничиваем права на уровне имени пользователя
    3.2. Ограничиваем права на уровне чата
  4. Заключение

Введение


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


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


Ограничить права можно не только на уровне пользователя, но и на уровне отдельных чатов.


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


  • say_hello команда приветствия
  • what_time команда, по которой бот сообщает текущую дату и время

Базовый код бота
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)}## команда по которой бот возвращает системную дату и времяwhat_time <- function(bot, update) {  # Запрашиваем текущее время  cur_time <- as.character(Sys.time())  # Отправка сообщения  bot$sendMessage(update$message$chat_id,                   text = paste0("Текущее время, ", cur_time),                                parse_mode = "Markdown",                                reply_to_message_id = update$message$message_id)}# обработчикиh_hello <- CommandHandler('say_hello', say_hello)h_time  <- CommandHandler('what_time', what_time)# добавляем обработчики в диспетчерupdater <- updater + h_hello + h_time# запускаем бота updater$start_polling()

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

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


Ограничиваем права пользователя с помощью фильтров сообщений


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


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


Ограничиваем права на уровне имени пользователя


Для создания собственных фильтров вам необходимо с помощью функции BaseFilter() добавить новый элемент в объект MessageFilters. Более подробно об этом я рассказывал во второй статье из данной серии.


В анонимную функцию, которую вы прописываете внутри BaseFilter() передаётся всего один аргумент message. Это сообщение которое вы, или другой пользователей отправляет боту, со всеми его метаданными. Данный объект имеет следующую структуру:


$message_id[1] 1174$from$from$id[1] 194336771$from$is_bot[1] FALSE$from$first_name[1] "Alexey"$from$last_name[1] "Seleznev"$from$username[1] "AlexeySeleznev"$from$language_code[1] "ru"$chat$chat$id[1] 194336771$chat$first_name[1] "Alexey"$chat$last_name[1] "Seleznev"$chat$username[1] "AlexeySeleznev"$chat$type[1] "private"$date[1] 1601295189$text[1] "отправленный пользователем текст"$chat_id[1] 194336771$from_user[1] 194336771

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


## список пользователей, с полными правамиMessageFilters$admins <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$from$username %in% c('AlexeySeleznev', 'user1', 'user2') })

Где c('AlexeySeleznev', 'user1', 'user2') вектор, с именами пользователей, которые могут использовать все функции бота. Далее этот фильтр мы используем при создании обработчиков.


## фильтр для вызова команды say_helloMessageFilters$say_hello <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/say_hallo'  })## фильтр для вызова команды what_timeMessageFilters$what_time <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/what_time'  })# обработчикиh_hello <- MessageHandler(say_hello, MessageFilters$admins & MessageFilters$say_hello)h_time  <- MessageHandler(what_time, MessageFilters$admins & MessageFilters$what_time)

Теперь нашего бота могут использовать пользователи с логинами AlexeySeleznev, user1, user2. На сообщения отправленные боту другими пользователями он никак не будет реагировать.


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


Код бота, в котором с помощью фильтров был ограничен круг пользователей
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)}## команда по которой бот возвращает системную дату и времяwhat_time <- function(bot, update) {  # Запрашиваем текущее время  cur_time <- as.character(Sys.time())  # Отправка сообщения  bot$sendMessage(update$message$chat_id,                   text = paste0("Текущее время, ", cur_time),                                parse_mode = "Markdown",                                reply_to_message_id = update$message$message_id)}# фильтры## список пользователей, с полными правамиMessageFilters$admins <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$from$username %in% c('AlexeySeleznev', 'user1', 'user2') })## фильтр для вызова команды say_helloMessageFilters$say_hello <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/say_hallo'  })## фильтр для вызова команды what_timeMessageFilters$what_time <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/what_time'  })# обработчикиh_hello <- MessageHandler(say_hello, MessageFilters$admins & MessageFilters$say_hello)h_time  <- MessageHandler(what_time, MessageFilters$admins & MessageFilters$what_time)# добавляем обработчики в диспетчерupdater <- updater + h_hello + h_time# запускаем бота updater$start_polling()

Ограничиваем права на уровне чата


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


## список чатов в которых разрешено использовать ботаMessageFilters$chats <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$chat_id %in% c(194336771, 0, 1)  })## фильтр для вызова команды say_helloMessageFilters$say_hello <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/say_hallo'  })## фильтр для вызова команды what_timeMessageFilters$what_time <- BaseFilter(  function(message) {    # проверяем от кого отправлено сообщение    message$text == '/what_time'  })# обработчикиh_hello <- MessageHandler(say_hello, MessageFilters$admins & MessageFilters$chats & MessageFilters$say_hello)h_time  <- MessageHandler(what_time, MessageFilters$admins & MessageFilters$chats & MessageFilters$what_time)

Ограничиваем права пользователя внутри кода методов


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


Ограничиваем права на уровне имени пользователя


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


# функция для проверки прав пользователяbot_check_usernames <-   function(admins, username) {   username %in% admins }

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


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


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


Код бота, в котором ограничены права пользователя внутри кода методов
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# Пишем метод для приветсвия## команда приветствияsay_hello <- function(bot, update) {  # Имя пользователя с которым надо поздароваться  user_name <- update$message$from$username  # проверяем разрешено ли использовать данному пользователю этот метод  if ( bot_check_usernames(c('AlexeySeleznev', 'user1', 'user2'), user_name) ) {    # Отправка сообщения    bot$sendMessage(update$message$chat_id,                     text = paste0("Моё почтение, ", user_name, "!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  } else {    # Отправка сообщения    bot$sendMessage(update$message$chat_id,                     text = paste0("У вас нет прав для использования этого метода!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  }}## команда по которой бот возвращает системную дату и времяwhat_time <- function(bot, update) {  # проверяем разрешено ли использовать данному пользователю этот метод  if ( bot_check_usernames(c('user1', 'user2'), update$message$from$username) ) {    # Запрашиваем текущее время    cur_time <- as.character(Sys.time())    # Отправка сообщения о том что у пользователя не достаточно прав    bot$sendMessage(update$message$chat_id,                     text = paste0("Текущее время, ", cur_time),                                  parse_mode = "Markdown",                                  reply_to_message_id = update$message$message_id)  } else {    # Отправка сообщения о том что у пользователя не достаточно прав    bot$sendMessage(update$message$chat_id,                     text = paste0("У вас нет прав для использования этого метода!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  }}# обработчикиh_hello <- CommandHandler('say_hello', say_hello)h_time  <- CommandHandler('what_time', what_time)# добавляем обработчики в диспетчерupdater <- updater + h_hello + h_time# запускаем бота updater$start_polling()

Результат будет следующим:


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


Ограничиваем права на уровне чата


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


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


bot_check_chat_id <-   function(allowed_chats, current_chat) {     current_chat %in% allowed_chats }

Далее используем эту функции внутри наших методов:


Код бота с распределением прав пользователя на уровне пользователей и чатов
library(telegram.bot)# создаём экземпляр класса Updaterupdater <- Updater('ТОКЕН ВАШЕГО БОТА')# Пишем метод для приветсвия## команда приветствияsay_hello <- function(bot, update) {  # Имя пользователя с которым надо поздароваться  user_name <- update$message$from$username  # проверяем разрешено ли использовать данному пользователю этот метод  if ( bot_check_usernames(c('AlexeySeleznev', 'user1', 'user2'), user_name)        &       bot_check_chat_id(c(194336771, 1, 2), update$message$chat_id)) {    # Отправка сообщения    bot$sendMessage(update$message$chat_id,                     text = paste0("Моё почтение, ", user_name, "!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  } else {    # Отправка сообщения    bot$sendMessage(update$message$chat_id,                     text = paste0("У вас нет прав для использования этого метода!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  }}## команда по которой бот возвращает системную дату и времяwhat_time <- function(bot, update) {  # проверяем разрешено ли использовать данному пользователю этот метод  if ( bot_check_usernames(c('AlexeySeleznev', 'user1', 'user2'), update$message$from$username)       &       bot_check_chat_id(c(194336771, 1, 2), update$message$chat_id)) {    # Запрашиваем текущее время    cur_time <- as.character(Sys.time())    # Отправка сообщения о том что у пользователя не достаточно прав    bot$sendMessage(update$message$chat_id,                     text = paste0("Текущее время, ", cur_time),                                  parse_mode = "Markdown",                                  reply_to_message_id = update$message$message_id)  } else {    # Отправка сообщения о том что у пользователя не достаточно прав    bot$sendMessage(update$message$chat_id,                     text = paste0("У вас нет прав для использования этого метода!"),                    parse_mode = "Markdown",                    reply_to_message_id = update$message$message_id)  }}# обработчикиh_hello <- CommandHandler('say_hello', say_hello)h_time  <- CommandHandler('what_time', what_time)# добавляем обработчики в диспетчерupdater <- updater + h_hello + h_time# запускаем бота updater$start_polling()

Заключение


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


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

Подробнее..

MS SQL Telegram ?

20.10.2020 10:16:23 | Автор: admin
image

Привет Хабр!


Мы с моим приятелем и занимаемся разработкой и поддержкой решений на MS SQL.


Одной из наших проблем при работе с MS SQL было то, что без MS SQL Management Studio невозможно что-то сделать или проверить. А значит всегда нужен под рукой компьютер и VPN в сеть клиента (или что посложнее). Нас это не устраивало мы не всегда сидим перед монитором, а ситуации ингода требуют оперативности, особенно когда дело касается высоких нагрузок в real-time системах. Поэтому мы решили сделать телеграм-бота SQL. (Поискали готовые варианты на Github, но, увы, не нашли того, что нам подошло бы).


Штука оказалась удобной. Вот что она умеет:


  1. Для работы нужен только SQL сервер от 2016 и выше (никаких доп. сервисов поднимать не надо)
  2. Можно явно привязать своего бота к своему чату (как, есть в инструкции в репозитории). Т.е. никто кроме вас с вашим сервером общаться не сможет.
  3. Бот умеет выполнять SQL запросы и структурировано показывать результат:
    select
  4. Бот умеет выполнять быстрые команды (их можно сконфигурировать самостоятельно) и выводить результат:
    select
  5. Бот умеет показывать прогресс бар для долгих процессов (например backup/restore)
    backup
  6. Дополнительно:
    a. В комплект с ботом идет простая система мониторинга состояния MS SQL Server
    b. Бот берет из системы мониторинга критическую статистику и публикует в чат

Мы выложили исходники и инструкции по установке на github в открытый доступ.


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


П.С.
Хочу выразить благодарносить t0rr, одному из мейнтейнеров aiogram за консультацию и вдохновение на начальном этапе создания

Подробнее..

No-code в действии мастерим временный email-адрес

23.03.2021 14:22:27 | Автор: admin

No-code сейчас в тренде. Статей на эту тему пока не много, хотя они появляются достаточно регулярно. На Хабре по тегу no-code и его вариантам я нашел всего около 15 статей и первая из них появилась только в июне 2020 меньше года назад! Во время чтения одной из статей у меня возникла идея собрать разные варианты no-code сценариев и снабдить некоторых из них, наиболее востребованных, инструкциями по реализации. Мне кажется, это будет интересно многим. Внизу после туториала, вы найдете пока небольшой, но пополняемый список сценариев и опрос, а пока давайте посмотрим как реализовать один простой сценарий.

Он относительно многоцелевой и с его помощью можно сделать различные интеграции для электронной почты, в том числе временный адрес почты для регистрации на разных сайтах. Если вы считаете, что можно просто зарегистрировать очередной ящик на gmail, или если вы знаете реализации такого сервиса, можете написать об этом в комментарии и дальше не читать. Наверняка, можно сделать это проще, нужно меньше 10 строчек кода. Хорошо, если хотите, напишите и об этом. Ну а для остальных, кому интересны технологии no-code, но пока не доходили руки разобраться и что-то сконструировать самому, предлагаю подробное описание сценария и пошаговую инструкцию.

Описание задачи

Предположим, вам нужен временный адрес, который не жалко засветить при регистрации на малоизвестном сайте или сайте с репутацией, вызывающей вопросы. После регистрации или когда надобность в адресе отпадет, можем удалить или оставить, но временно заблокировать его (см. об этом шаг 10c ниже) вдруг нужно будет позже восстановить пароль. Идея простая и очевидная и такие сервисы наверняка есть, хотя я сам ими не пользовался. Говорят, что существует такой сервис у Apple, когда при регистрации с помощью Apple Id, он предлагает подменить основной почтовый адрес временным. Хорошая идея, но в данном случае она доступна только владельцам яблочных гаджетов, более того, сайт на котором нужна регистрация, должен принимать Apple Id. Также я сам видел бота, который предлагал временный адрес почты. Это был простейший бот, но он почему-то не работал.

Получаются два стартовых условия: 1) делаем свой, кастомный и настраиваемый сценарий; подробнее об этом см. варианты развития сценария шаг 11 почти в самом конце; 2) обходимся без единой строчки кода.

Конечно, сценарий будет зависеть от сторонних сервисов и их поставщиков, а также будет, скорее всего, платным. Но есть и хорошие новости, он может быть создан на коленке за считанные минуты. В самом худшем случае, если вы делаете это первый раз или если вдруг что-то пойдет не так, за 1-2 часа максимум. В последние несколько лет количество новых no-code сервисов растет как на дрожжах, так что сценарий может быть реализован разными способами и мы можем выбирать наиболее удобный вариант и таким образом снизить зависимость от провайдеров no-code. Поэтому добавляем еще условие: 3) задействованные сервисы должны быть легко заменяемыми и настраиваемыми. В одной статье не получится полностью описать, как реализовать все 3 условия, но будем считать это заделом на будущее развитие сценария (шаг 11). Кроме того, сейчас не будем подробно сравнивать разные альтернативы и объяснять, почему именно эти варианты выбраны. Об этом есть множество других публикаций (примеры есть по ссылкам в следующем абзаце) и, конечно же, можете написать обо всех альтернативах в комментариях.

Альтернативы no-code

Если вы в первый раз слышите о no-code, возможно вам будет интересно почитать вводные обзоры и статьи для знакомства с отдельными сервисами. Для старта подойдет небольшой обзор No-code как отличная альтернатива для быстрого решения бизнес-задач. Взвешиваем pro et contra Движение No-code конец программистов? Разбираем плюсы и минусы. Введение в один из инструментов на Хабре n8n. Автоматизация ИБ со вкусом смузи.

Мы подходим к задаче серьезно, поэтому изучив и выбрав лучшие (на сегодняшний день) из доступных альтернатив, уточним ТЗ.

Техническое задание

Нужно получить временный адрес и указать его при регистрации на каком-то левом сайте/сервисе сомнительного качества или с репутацией, вызывающей вопросы. И далее мы должны будем получить письмо или как-то прочитать его. Нас интересует только ссылка или код регистрации в теле письма, поэтому достаточно его извлечь и переслать дальше. Вот только куда? На основной адрес? Но зачем нам это письмо на почте, может лучше в виде короткого сообщения в чате или даже СМС-ки. Можно подключить WhatsApp, Slack или другой мессенджер. Но удобнее всего, конечно, Телеграм, для него и сделаем сценарий.

Проблемы, которые должен решать сценарий: 1) в общем случае текст письма может быть длинным, в то время как текст сообщения ТМ ограничен до 4096 символов, нужно обрезать длинные письма или разбивать их на части, 2) нам нужно также извлечь из письма ссылку или код для регистрации, т.е. нужно уметь обработать не только plain-text, но и HTML.

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

Шаг 1. Регистрируемся в Zapier

Если у вас есть аккаунт, этот шаг пропускаем. Если нет, нужно будет пройти стандартную процедуру регистрации на сайте Zapier и выбрать тарифный план или тестовый период. К сожалению, я регистрировался давно и точных условий сейчас не знаю, кажется, бесплатный тариф позволяет выполнить до 100 операций в месяц. Это совсем мало, но для тестирования сценария хватит. К тому же, посчитайте, как часто вы регистрируетесь на разных сайтах? И если этот сценарий придется вам по вкусу и вы захотите его и адрес почты использовать на всю катушку, можно будет попробовать заменить Zapier на другой сервис (например, бесплатный n8n) или заплатить за тариф, который вас устроит.

Шаг 2. Создайте новый Zap

Нажимаем [MAKE A ZAP] см. скриншот

Шаг 3: Укажите название запа и выберите триггер

Нужно как-то назвать Зап/сценарий и настроить триггер, который его запускает.

a) Жмем на Name your zap и вводим название запа. Вы можете выбрать любое. Я назвал его TMP Email Zap.
b) Далее в строке поиска вводим: email, Zapier покажет доступные почтовые сервисы.
c) В качестве триггера доступны различные приложения, но они потребуют дополнительных действий и регистрации новых сервисов, скорее всего. Нам не нужны такие трудности, выбираем простейший и первый в списке вариант Email by Zapier. См. скриншот выше.

Шаг 4: Выберите событие триггера

a) В разделе Trigger event нажмите на Choose an event
b) Тут вариантов не много: выбираем New inbound Email. См. скриншот выше.
c) Нажмите дальше [Continue]

Шаг 5: Выберите адрес email

Укажите адрес почты:

a) Появится поле для ввода email-адреса. Можете добавить любое слово. Но важно использовать ТОЛЬКО буквы в нижнем регистре или цифры, иначе вы не сможете пройти дальше.
b) Сохраните полученный адрес (кнопка [Copy]), вы будете использовать его для регистрации на левом сайте.
c) Нажмите дальше [Continue]. См. скриншот выше.

ВНИМАНИЕ!!!

Не показывайте никому этот адрес. Иначе вы рискуете попасть на спам. После чего, в лучшем случае, вы превысите бесплатные лимиты и сценарий перестанет работать, а в худшем случае придётся заплатить рублем, причём не деревянным, а зелёным по текущему курсу ЦБ. Хотя Zapier не очень дорогой, но платить только за спам совсем не хочется.

Шаг 6: Протестируйте email адрес

После того, как вы получили адрес почты, можете его протестировать.

a) Попробуйте отправить письмо на адрес, который вы получили на шаге 5b.
b) После отправки письма нажмите кнопку [Test Trigger]
c) Если ничего не происходит или Zapier пишет что-то вроде Request no found, подождите несколько секунд и еще раз нажмите кнопку письму нужно время, чтобы дойти до сервера Zapierа
d) Если письмо все еще не пришло, проверьте, правильно ли вы скопировали адрес
e) После того как Zapier получит тестовое письмо, он покажет содержание письмо и все доступные поля (sender, subject и пр. их довольно много).
f) После тестирования, нажмите кнопку [Continue]

Шаг 7: Настройте бота, получателя сообщения

Можно самостоятельно зарегистрировать бота есть множество инструкций, как это сделать. Но если оставаться строго в парадигме no-code, можно воспользоваться уже готовым решением телеграм-ботом.

a) Откройте Telegram и бота по этой ссылке @co_notbot. Если у вас еще нет Телеграма, его нужно установить.
b) При входе нажмите кнопку [Start] или введите команду /start. Ждите некоторое время пока бот отработает команду. Появится сообщение с Главным меню бота и две кнопки внизу.
c) Нажмите на кнопку [Подключить]. См. скриншот выше.
d) Откроется следующее окно, в котором появится больше вариантов и кнопок. Нас интересует кнопка [Webhook]. Нажмите на нее и подождите до 1-3 секунд. См. скриншот ниже:

e) В следующем сообщении от бота вы получите вебхук, который нужно будет скопировать и затем добавить в наш Zap на шаге 9a.

ВНИМАНИЕ!!!

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

Шаг 8: Выбираем Action

a) Вернитесь в Zapier и нажмите кнопку [Action].
b) В строке поиска наберите web
c) Из списка выберите Webhook by Zapier. См. скриншот выше

a) далее выберите Action event, нажмите на [Choose an Event]
b) из списка выберите вариант [POST]. См. на скриншоте выше.
c) Нажмите дальше [Continue]

Шаг 9: Добавьте вебхук и параметры

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

a) в поле [URL] введите адрес вебхука, который вы получили на шаге 7e. На скриншоте приведен пример. У вас должна быть точная копия с токеном и адресом.
b) в поле [Payload Type] оставьте вариант [Form]
c) в следующем разделе [Data] нужно будет добавить/задать передаваемые параметры вебхука. Нам нужно будет задать одно поле text и его значение, в котором будет передаваться сообщение.
d) в поле text можно сначала добавить значение [Subject], [Sender], [Body Plain] или [Stripped Text] из письма
e) если тестирование (шаг 10) пройдет успешно, можно попробовать обрабатывать и передавать значения из html (об этом см. шаг 11c). Можно пробовать разные варианты и смотреть, что получится в результате. Если после выполнения Zapier будет выдавать сообщения об ошибках, попробуйте задать статическую строку, например, Hello world! и посмотрите, что получится.
f) остальные поля, ниже раздела [Data], заполнять и изменять не нужно

Шаг 10: Тестирование отправки письма и всего сценария

a) после того как все поля будут заполнены вы можете снова протестировать отправку письма и весь сценарий. Отправьте новое тестовое письмо и нажмите кнопку [Test & Review]
b) если тест пройдет успешно, то вы получите ответ в зеленой зоне и сообщение вроде Test was successful включите Zap/сценарий, нажмите на переключатель [off] [on];
c) обратным действием можно выключить (или на время заблокировать) этот сценарий позже

Шаг 11: Развитие сценария: извлекаем ссылки, добавляем фильтр, укорачиваем текст

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

a) проверка длины текста и самостоятельно решаем обрезать текст или делить его на куски < 4096 символов, чтобы не превысить лимиты Телеграма; можно реализовать по-разному, модулем Formatter by Zapier, например;
b) можно пересылать не каждое сообщение, а пропускать все сообщения через фильтр, который оставит только нужное и уберет спам, например. См. Filter by Zapier;
c) можно сделать более сложную обработку входящих писем и извлекать ссылки и/или картинки из HTML кода письма (Formatter by Zapier). Как вариант, после этого картинки можно пропустить через один из сервисов распознавания изображений для извлечения чисел/текста/номеров/лиц и пр.
d) можно самостоятельно зарегистрировать бота Телеграм и подключить один из бот-конструкторов; и тогда сможем реализовать бота по-своему и не будем зависеть от работоспособности стороннего бота. Правда попадем в новую зависимость от сервиса-конструктора;
e) можно сделать новый чат, куда с помощью аналогичного вебхука вместо письма настроить получение RSS, уведомлений или любого другого потока сообщений;
f) и наконец, можно сделать отдельные шаги взаимозаменяемыми, чтобы не зависеть от отдельного провайдера сервиса no-code. Например, вместо Zapierа можно использовать n8n или Integromat.

Как я обещал, кратко перечислю более-менее простые сценарии no-code. Выберите наиболее интересные на ваш взгляд.

  • Кастомный фильтр спама на базе AI/ML

  • Временная почта для регистраций (см. пример этого выше)

  • Агрегатор и фильтр вакансий/новостей/объявлений/rss

  • Сканирование и учет чеков и финансовых операций

  • Кастомный uptime-мониторинг для сайтов, серверов

  • Уведомления и команды Умного дома

  • No-code решения для скилов Алексы или навыков Алисы

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

Подробнее..

Категории

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

  • Имя: Макс
    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