Skip to main content

Список задач

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

cmd

Скачать исходники можно здесь

Работа над этим проектом предполагает, что вы прошли главу веб. Скачать исходники предыдущего проекта можно здесь. Дамп базы данных dump.sql лежит в корне.

Документация Telegram API находится здесь

Первое приложение

Для начала необходимо добавить новые зависимости в файл pom.xml

        <dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots-spring-boot-starter</artifactId>
<version>5.2.0</version>
</dependency>

У телеграм есть два режима обработки запросов. Первый - это Long polling(длинные запросы). В этом режиме ваше приложение само будет опрашивать сервер telegram с определенной периодичностью, есть ли для него новые сообщения. Это происходит медленно. Второй - это Webhook(вебхук), переводится дословно как веб-крюк. В этом режиме сервер telegram моментально будет перенаправлять сообщения на указанный нами сервер.

Туннель

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

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

Если вы ничего не делали со своим роутером, у вас будут все порты открыты, и вам достаточно просто скачать программу-туннель, переходите просто к следующему абзацу. В противном случае вам необходимо зайти в настройки своего роутера и перенаправить все запросы по порту 80 на локальный ip вашего рабочего компьютера. Либо можете указать другой порт, но тогда и сервер необходимо будет поднимать на этом же порте. Для этого в файле настроек локального сервера application-dev.properties необходимо дописать строчку

server.port=5000

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

Мы будем использовать программу ngroc.

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

cmd

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

cmd

После успешной регистрации вас автоматически перенаправит в личный кабинет. Нажмите кнопку Download for Windows

cmd

Откройте скачанный архив

cmd

И извлеките программу в любое удобное для вас место. Я создал папку на диске D:\ и положил программу туда

cmd

Теперь нам надо запустить программу. Она написана для консоли, поэтому нам понадобится запустить PowerShell. Чтобы это сделать зажмите клавишу Shift и кликните правой кнопкой мыши по области просмотра файлов в проводнике. В появившемся меню выберите Открыть окно PowerShell здесь

cmd

Теперь введите команду

./ngrok.exe http 80 --host-header=site.local

cmd

Запустится сервер

cmd

Если вы поднимаете сервер на своём порте, то вместо 80 в команде укажите свой порт.

Место внешнего адреса вашего туннеля выделено красными уголками. Скопируйте этот адрес и добавьте в application-dev.properties строчку

telegrambot.webhookPath=TUNNEL_URL

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

telegrambot.webhookPath=https://5a34-78-37-233-119.ngrok.io

Этот адрес - временный, через два часа он изменится, поэтому не забывайте менять его в настройках application-dev.properties.

Сервер

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

В application-dev.properties мы запишем новые переменные

telegrambot.apiUrl=https://api.telegram.org/
telegrambot.userName=buran_test_bot
telegrambot.botToken=5242184597:**********NI

где переменной telegrambot.botToken присваивается токен, полученный от ботовода, в переменной telegrambot.userName указывается username бота, у меня он называется buran_test_bot

И сразу же пропишем эти же переменные для application-prod.properties:

telegrambot.apiUrl=https://api.telegram.org/
telegrambot.userName=buran_test_bot
telegrambot.botToken=${TELEGRAM_BOT_TOKEN}

Также добавим переменную пути к серверу обработки веб-хуков

telegrambot.webhookPath=${WEBHOOK_PATH}

Добавим их значения в переменные среды приложения:

cmd

Переменной WEBHOOK_PATH присвойте просто адрес вашего сайта, на котором развёртывается приложение.

В первую очередь создадим класс настроек телеграм бота TelegramBotConfig

cmd

package com.example.demo.config;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* Настройки телеграм-бота
*/
@Component
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBotConfig {
/**
* Путь до сервера обработки веб=хуков
*/
@Value("${telegrambot.webhookPath}")
String webHookPath;
/**
* Имя пользователя для бота
*/
@Value("${telegrambot.userName}")
String userName;
/**
* Токен бота
*/
@Value("${telegrambot.botToken}")
String botToken;
}

Здесь используются новые аннотации: @Getter формирует только геттеры, в отличие от @Data, также добавлена аннотация @FieldDefaults() - она автоматически задаёт модификаторы доступа всем полям. В нашем случае все поля будут интерпретироваться как закрытые(private).

Теперь добавим класс настроек приложения AppConfig

cmd

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

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

Глубоко разбирать суть происходящего не будем, достаточно знать, что два метода с аннотациями @Bean создадут спринг-объекты, с которыми можно уже работать

package com.example.demo.config;

import com.example.demo.telegram.TelegramBot;
import com.example.demo.telegram.TelegramFacade;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;

/**
* Настройки приложения
*/
@Configuration
public class AppConfig {
/**
* Настройки бота
*/
private final TelegramBotConfig botConfig;

/**
* Конструктор настроек приложения
*
* @param botConfig - настройки бота
*/
public AppConfig(TelegramBotConfig botConfig) {
this.botConfig = botConfig;
}

/**
* Бин обработки связывания веб-хука
*
* @return бин обработки связывания веб-хука
*/
@Bean
public SetWebhook setWebhookInstance() {
return SetWebhook.builder().url(botConfig.getWebHookPath()).build();
}

/**
* Бин телеграм-бота
*
* @param setWebhook - бин связывания веб-хука
* @return бин телеграм-бота
*/
@Bean
public TelegramBot springWebhookBot(SetWebhook setWebhook) {
// создаём бота
TelegramBot bot = new TelegramBot(setWebhook);
// заполняем значениями его поля
bot.setBotToken(botConfig.getBotToken());
bot.setBotUsername(botConfig.getUserName());
bot.setBotPath(botConfig.getWebHookPath());
// возвращаем бота
return bot;
}
}

Теперь пропишем контроллер. По адресу / у нас обрабатывается только GET-запрос, который возвращает главную страницу сайта. Поэтому можно повесить обработку веб-хуков на него же, ведь они обрабатываются с помощью POST-запросов.

Создадим новый контроллер

cmd

package com.example.demo.controller;

import com.example.demo.telegram.TelegramBot;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Контроллер веб-хуков
*/
@RestController
public class WebhookController {

/**
* Телеграм-бот
*/
private final TelegramBot telegramBot;

/**
* Конструктор контроллера веб-хуков
* @param telegramBot - телеграм-бот
*/
public WebhookController(TelegramBot telegramBot) {
this.telegramBot = telegramBot;
}

/**
* Обработчик запросов
* @param update - тело запроса
* @return апи-метод
*/
@PostMapping("/")
public BotApiMethod<?> onUpdateReceived(@RequestBody Update update) {
return telegramBot.onWebhookUpdateReceived(update);
}
}

У метод onUpdateReceived() возвращаемое значение BotApiMethod<?> может показаться очень странным. В действительности знак ? - это некий аналог шаблона(generic), но без базового класса. Можно было бы это же показать через Object, но иногда правильнее писать так.

Подробнее можно прочитать здесь

Теперь пропишем самого бота. Создадим класс TelegramBot в пакете telegram

cmd

package com.example.demo.telegram;

import com.example.demo.entities.User;
import com.example.demo.service.UserService;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.extern.java.Log;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.starter.SpringWebhookBot;

/**
* Класс телеграм-бота
*/
@Getter
@Setter
@Log
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBot extends SpringWebhookBot {
/**
* Пусть к боту
*/
String botPath;
/**
* username бота
*/
String botUsername;
/**
* Токен бота
*/
String botToken;

/**
* Конструктор телеграм-бота
*
* @param options - параметры
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
}

/**
* Конструктор телеграм-бота
*
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(SetWebhook setWebhook) {
super(setWebhook);
}

/**
* Обработчик запросов от телеграм-сервера
*
* @param update - объект запроса
* @return - объект запроса API телеграма
*/
@Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) {
// Если в запросе есть `Callback`
if (update.hasCallbackQuery()) {
// пока что просто получаем его объект
CallbackQuery callbackQuery = update.getCallbackQuery();
return null;
} else {
// получаем сообщение
Message message = update.getMessage();
// получаем имя пользователя
String telegramUserName = message.getFrom().getFirstName();
// если у сообщения есть текст
if (message.hasText()) {
// формируем ответ
SendMessage sendMessage = new SendMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText("Привет, " + telegramUserName + "!\nВы написали:\n" + message.getText());
// возвращаем ответное сообщение
return sendMessage;
}
}
return null;
}
}

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

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

Перезапустите сервер.

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

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

cmd

и нажать Enter

Строка составляется так:

https://api.telegram.org/bot{TOKEN}/setWebhook?url={WEBHOOK_URL}

Например, если токен {TOKEN} равен 5242184597:********_NI, а адрес сервера {WEBHOOK_URL} равен https://b973-78-37-233-119.ngrok.io, то запрос на связывание вебхука будет таким:

https://api.telegram.org/bot5242184597:********_NI/setWebhook?url=https://b973-78-37-233-119.ngrok.io

Если строка составлена правильно, то телеграм-сервер выдаст такой ответ

cmd

Попробуйте теперь найти своего бота в телеграм и написать ему какое-нибудь сообщение. Если всё настроено правильно, он должен вам ответить

cmd

Развёртывание

Теперь развернём нашего бота на платформе heroku. Не забудьте перед развёртывание поменять режим в application.properties на prod

cmd

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

cmd

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

cmd

Только в этот раз необходимо указать путь до вашего сайта, в моём случае - это https://buran-tbot.herokuapp.com/

Если данные указаны верно, то система снова вернёт сообщение о том, что веб-хуки связаны

cmd

Остановите свой локальный сервер и попробуйте снова написать сообщение боту. Бот должен продолжить работать.

cmd

при этом ответ стал ощутимо быстрее. Это происходит не просто так. У heroku довольно мощные машины и быстрый канал связи.

Скачать исходники можно здесь

Авторизация

Чтобы связать пользователя телеграм с нашим сайтом, добавим в таблицу пользователя новую колонку(поле) tusername, которая будет хранить имя пользователя телеграм

Модель

Для этого откройте вкладку работы с базой данных, кликните правой кнопкой мыши то таблице пользователей user_table и выберите пункт Modify table

cmd

В новом окне нажмите на знак + и заполните поля, как на скрине. в Поле Default: надо вставить просто две одинарные кавычки ''.

cmd

После этого нажмите Execute

После этого нажмите на значок обновления

cmd

В таблице появится новая колонка.

Добавим в класс User соответствующее поле

    /**
* Имя телеграм пользователя
*/
private String tusername;

Теперь нам надо дописать в репозиторий пользователей UserRepository метод поиска всех пользователей по полю tusername

    /**
* Получить всех пользователей по заданному имени пользователя в телеграмме
*
* @param tusername - имя пользователя в телеграмме
* @return список пользователей
*/
@Query("SELECT u FROM User u WHERE u.tusername= ?1")
List<User> findAllByTUsername(String tusername);

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

Запрос на поиск всех записей с заданным критерием строится так. Сначала идёт команда SELECT, потом указывается, что именно мы извлекаем, потом указывается ключевое слово FROM после него в оригинальном запросе указывается название таблицы, в адаптации спринг мы вместо таблицы указываем название класса, соответствующей сущности и переменную, которая будет добавляться в список. Дальше идёт блок условий, начинающийся с ключевого слова WHERE после этого вводится маска ?1 - в это место подставляется первый аргумент, если нужно передать ещё параметры, просто допишите их в аргументах, а в маску добавьте ?2, ?3 и т.д.

Теперь в сервис работы с пользователями UserService добавим метод получения пользователя по имени пользователя в телеграм

    /**
* Получить пользователя по его имени
*
* @param username - имя
* @return пользователь
* @throws UsernameNotFoundException - исключение, если пользователь не найден
*/
public UserDetails loadUserByTUsername(String username) throws UsernameNotFoundException {
List<User> users = userRepository.findAllByTUsername(username);
if (users == null || users.isEmpty()) {
throw new UsernameNotFoundException("User not found");
}
return users.get(0);
}

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

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

cmd

package com.example.demo.telegram;

/**
* Множество состояний бота
*/
public enum BotState {
/**
* Начальное состояние
*/
STATE_START,
}

Пока что добавим в него только одно состояние STATE_START

Контроллер

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

Поэтому под каждый вид задач лучше создавать отдельные класс-сервис. Для данного проекта мы пропишем всего один сервис RegularOperations.

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

Пока что создадим только сервис RegularOperations в пакете operations

cmd

package com.example.demo.telegram.operations;

import org.springframework.stereotype.Service;
import org.telegram.telegrambots.meta.api.objects.Message;

/**
* Сервис обычных операций
*/
@Service
public class RegularOperations {
/**
* Операция старта
*
* @param message - пришедшее сообщение
* @return ответ
*/
public String start(Message message) {
return "Привет, " + message.getFrom().getFirstName() + "!\nВы написали:\n" + message.getText();
}
}

Теперь в самом боте TelegramBot добавим автоматически связываемые поля сервиса операций и состояния бота

    /**
* Сервис обычных операций
*/
@Autowired
RegularOperations regularOperations;
/**
* Состояние бота
*/
BotState botState;
Инициализация

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

    /**
* Конструктор телеграм-бота
*
* @param options - параметры
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
this.botState = BotState.STATE_START;
}

/**
* Конструктор телеграм-бота
*
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(SetWebhook setWebhook) {
super(setWebhook);
this.botState = BotState.STATE_START;
}

Теперь перепишем метод onWebhookUpdateReceived():

 /**
* Обработчик запросов от телеграм-сервера
*
* @param update - объект запроса
* @return - объект запроса API телеграма
*/
@Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) {
// Если в запросе есть `Callback`
if (update.hasCallbackQuery()) {
// пока что просто получаем его объект
CallbackQuery callbackQuery = update.getCallbackQuery();
return null;
} else {
// получаем сообщение
Message message = update.getMessage();
// получаем имя пользователя
String telegramUserName = message.getFrom().getFirstName();
try {
User u = (User) userService.loadUserByTUsername(telegramUserName);
log.info(u + "");
} catch (Exception e) {
log.info(e + "");
}
log.info(message.getText());
// если у сообщения есть текст
if (message.hasText()) {
// формируем ответ
SendMessage sendMessage = new SendMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText(switch (botState) {
case STATE_START -> regularOperations.start(message);
});
// возвращаем ответное сообщение
return sendMessage;
}
}
return null;
}

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

Само формирование сообщения тоже немного изменено, теперь в зависимости от состояния с помощью switch в лямбда-стиле мы будем вызывать метод того или иного сервиса.

Перезапустите сервис и проверьте, что всё работает.

Я написал боту сообщение тест, он мне ответил так же как раньше.

cmd

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

Вход

Для регистрации нам необходимо добавить четыре состояния:

package com.example.demo.telegram;

/**
* Множество состояний бота
*/
public enum BotState {
/**
* Начальное состояние
*/
STATE_START,
/**
* бот ждёт логина
*/
STATE_WAIT_FOR_USERNAME,
/**
* бот ждёт пароля
*/
STATE_WAIT_FOR_PASSWORD,
/**
* бот связан с учётной записью пользователя
*/
STATE_CONNECTED
}

Для начала допишем сохранение пользователя с новым tusername в UserService

    /**
* Сохранить пользователя
*
* @param user - пользователь
* @param tusername - логин в телеграме
* @return флаг, получилось ли сохранить
*/
public boolean saveUser(User user, String tusername) {
user.setTusername(tusername);
userRepository.save(user);
return true;
}

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

package com.example.demo.telegram;

import com.example.demo.entities.Role;
import com.example.demo.entities.User;
import com.example.demo.service.UserService;
import com.example.demo.telegram.operations.RegularOperations;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.starter.SpringWebhookBot;

/**
* Класс телеграм-бота
*/
@Getter
@Setter
@Log
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBot extends SpringWebhookBot {
/**
* Пусть к боту
*/
String botPath;
/**
* username бота
*/
String botUsername;
/**
* Токен бота
*/
String botToken;
/**
* Сервис для работы с пользователями
*/
@Autowired
UserService userService;
/**
* Сервис обычных операций
*/
@Autowired
RegularOperations regularOperations;
/**
* Объект для работы с паролями
*/
final BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* Состояние бота
*/
BotState botState;
/**
* Имя пользователя для подключения
*/
User connectedUser;

/**
* Конструктор телеграм-бота
*
* @param options - параметры
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
this.botState = BotState.STATE_START;
this.bCryptPasswordEncoder = new BCryptPasswordEncoder();
}

/**
* Конструктор телеграм-бота
*
* @param setWebhook - бин связывания веб-хуков
*/
public TelegramBot(SetWebhook setWebhook) {
super(setWebhook);
this.botState = BotState.STATE_START;
this.bCryptPasswordEncoder = new BCryptPasswordEncoder();
}

/**
* Обработчик запросов от телеграм-сервера
*
* @param update - объект запроса
* @return - объект запроса API телеграма
*/
@Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) {
// Если в запросе есть `Callback`
if (update.hasCallbackQuery()) {
// пока что просто получаем его объект
CallbackQuery callbackQuery = update.getCallbackQuery();
return null;
} else {
// получаем сообщение
Message message = update.getMessage();
// если у сообщения есть текст
if (message.hasText()) {
// формируем ответ
SendMessage sendMessage = new SendMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText(switch (botState) {
case STATE_START -> processStart(message);
case STATE_WAIT_FOR_USERNAME -> waitForUsername(message);
case STATE_WAIT_FOR_PASSWORD -> waitForPassword(message);
case STATE_CONNECTED -> processConnected(message);
});
// возвращаем ответное сообщение
return sendMessage;
}
}
return null;
}

/**
* Обработка начального состояния
*
* @param message - сообщение
* @return ответ
*/
private String processStart(Message message) {
try {
// пытаемся найти сохранённого пользователя в базе
User u = (User) userService.loadUserByTUsername(message.getFrom().getUserName());
botState = BotState.STATE_CONNECTED;
return "Здравствуйте, " + message.getFrom().getFirstName() + ".\n" +
"Ваш логин в системе " + u.getUsername();
} catch (Exception e) {
log.info(e.getMessage());
botState = BotState.STATE_WAIT_FOR_USERNAME;
return "Вы не авторизованы, введите ваш логин";
}
}

/**
* Обработка состояния ожидания логина
*
* @param message - сообщение
* @return ответ
*/
private String waitForUsername(Message message) {
try {
connectedUser = (User) userService.loadUserByUsername(message.getText());
botState = BotState.STATE_WAIT_FOR_PASSWORD;
return "Введите пароль";
} catch (Exception e) {
return "Пользователь с логином " + message.getText() + " не найден\nпопробуйте ещё раз";

}
}

/**
* Обработка состояния ожидания пароля
*
* @param message - сообщение
* @return ответ
*/
private String waitForPassword(Message message) {
if (connectedUser == null)
return "Ошибка: пользователь не найден";
// если пароли совпадают
if (bCryptPasswordEncoder.matches(message.getText(), connectedUser.getPassword())) {
userService.saveUser(connectedUser, message.getFrom().getUserName());
botState = BotState.STATE_CONNECTED;
return "Связывание выполнено";
} else {
return "Неверный пароль";
}
}

/**
* Подключённое состояние
*
* @param message - сообщение
* @return ответ
*/
private String processConnected(Message message) {
return regularOperations.hello(message);
}
}

Теперь вход заработает

cmd

В таблице появится новое значение

cmd

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

cmd

Список дел

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

Для начала необходимо добавить новое состояние в множество состояний бота BotState

    /**
* бот ожидает нового задания
*/
STATE_WAIT_FOR_TASK

Теперь дополним сервис обычных операций

package com.example.demo.telegram.operations;

import com.example.demo.entities.Tasks;
import com.example.demo.entities.User;
import com.example.demo.service.TasksService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.telegram.telegrambots.meta.api.objects.Message;

import java.util.List;

/**
* Сервис обычных операций
*/
@Service
@Log
public class RegularOperations {
/**
* Сервис задач
*/
TasksService tasksService;

/**
* Конструктор сервиса обычных операций
*
* @param tasksService - сервис задач
*/
@Autowired
RegularOperations(TasksService tasksService) {
this.tasksService = tasksService;
}

/**
* Операция старта
*
* @param message - пришедшее сообщение
* @return ответ
*/
public String hello(Message message) {
return "Привет, " + message.getFrom().getFirstName() + "!\n" +
"Вы написали:\n" + message.getText();
}

/**
* Добавить задание
*
* @param message сообщение с текстом
* @param connectedUser подключенный пользователь
* @return ответ
*/
public String addTask(Message message, User connectedUser) {
Tasks task = new Tasks();
task.setTitle("Задача от бота");
task.setAuthor(connectedUser);
task.setText(message.getText());
tasksService.create(task);
return "Задача добавлена";
}

/**
* Отобразить список заданий
*
* @param connectedUser подключенный пользователь
* @return ответ
*/
public String listTask(User connectedUser) {
String result = "Задачи:\n\n";
// получаем список задачам конкретного пользователя
List<Tasks> tasksList = tasksService.readAll().stream().filter(
t -> t.getAuthor().getId().equals(connectedUser.getId())
).toList();
// формируем строку вывода
for (Tasks task : tasksList) {
result += ">" + task.getTitle() + "\n" +
task.getText() + "\n\n";
}
result +="\n";
return result;
}

}

Введём константы команд в TelegramBot

    /**
* Команда старта
*/
public static final String COMMAND_ADD_TASK = "/addTask";
/**
* Команда старта
*/
public static final String COMMAND_LIST_TASK = "/listTask";

Добавим обработку этого состояния ожидания задания

            if (message.hasText()) {
// формируем ответ
SendMessage sendMessage = new SendMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText(switch (botState) {
case STATE_START -> processStart(message);
case STATE_WAIT_FOR_USERNAME -> waitForUsername(message);
case STATE_WAIT_FOR_PASSWORD -> waitForPassword(message);
case STATE_CONNECTED -> processConnected(message);
case STATE_WAIT_FOR_TASK -> waitForTask(message);
});
// возвращаем ответное сообщение
return sendMessage;
}

Теперь допишем два метода для работы с задачами

    /**
* Обработка состояния ожидания задачи
*
* @param message - сообщение
* @return ответ
*/
private String waitForTask(Message message) {
regularOperations.addTask(message, connectedUser);
botState = BotState.STATE_CONNECTED;
return "Задача добавлена";
}

/**
* Запустить режим ожидания задачи
*
* @return ответ бота
*/
private String doWaitForTask() {
botState = BotState.STATE_WAIT_FOR_TASK;
return "Введите текст задачи";
}

и перепишем обработку сообщений в подключенном состоянии

    /**
* Подключённое состояние
*
* @param message - сообщение
* @return ответ
*/
private String processConnected(Message message) {
return switch (message.getText()) {
case COMMAND_ADD_TASK -> doWaitForTask();
case COMMAND_LIST_TASK -> regularOperations.listTask(connectedUser);
default -> regularOperations.hello(message);
};
}

Перезапустите сервер и попробуйте добавить задачу, а после вывести их список.

cmd

Если теперь перейти на сайт, то там тоже отобразится новая задача

cmd

Кнопки

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

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

Клавиатура ответа

Сначала напишем метод, который будет добавлять к ответу SendMessage клавиатуру ответа в сервисе обычных операций

    /**
* Отобразить меню
*
* @param sendMessage ответное сообщение
*/
public void showReplyMenu(SendMessage sendMessage) {
// создаём разметку клавиатуры ответа
ReplyKeyboardMarkup replyKeyboardMarkup = new ReplyKeyboardMarkup();
// разрешаем клавиатуру
replyKeyboardMarkup.setSelective(true);
// разрешаем её масштабирование
replyKeyboardMarkup.setResizeKeyboard(true);
// создаём список строк клавиатуры
List<KeyboardRow> keyboard = new ArrayList<>();
// создаём строки, в каждой по одной кнопке
KeyboardRow row1 = new KeyboardRow();
KeyboardRow row2 = new KeyboardRow();
row1.add(new KeyboardButton(COMMAND_ADD_TASK));
row2.add(new KeyboardButton(COMMAND_LIST_TASK));
// добавляем строки в список строк
keyboard.add(row1);
keyboard.add(row2);
// сохраняем клавиатуру в разметку
replyKeyboardMarkup.setKeyboard(keyboard);
// сохраняем разметку
sendMessage.setReplyMarkup(replyKeyboardMarkup);
}

Теперь добавим вывод клавиатуры в двух методах:

    /**
* Обработка начального состояния
*
* @param message - сообщение
* @return ответ
*/
private String processStart(Message message, SendMessage sendMessage) {
try {
// пытаемся найти сохранённого пользователя в базе
connectedUser = (User) userService.loadUserByTUsername(message.getFrom().getUserName());
botState = BotState.STATE_CONNECTED;
regularOperations.showReplyMenu(sendMessage);
return "Здравствуйте, " + message.getFrom().getFirstName() + ".\n" +
"Ваш логин в системе " + connectedUser.getUsername();
} catch (Exception e) {
log.info(e.getMessage());
botState = BotState.STATE_WAIT_FOR_USERNAME;
return "Вы не авторизованы, введите ваш логин";
}
}
...
/**
* Обработка состояния ожидания пароля
*
* @param message - сообщение
* @return ответ
*/
private String waitForPassword(Message message, SendMessage sendMessage) {
if (connectedUser == null)
return "Ошибка: пользователь не найден";
// если пароли совпадают
if (bCryptPasswordEncoder.matches(message.getText(), connectedUser.getPassword())) {
log.info(userService.saveUser(connectedUser, message.getFrom().getUserName()) + "");
botState = BotState.STATE_CONNECTED;
regularOperations.showReplyMenu(sendMessage);
return "Связывание выполнено";
} else {
return "Неверный пароль";
}
}

Теперь перезапустим бота:

cmd

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

Встроенная клавиатура

Чтобы добавлять встроенную клавиатуру к сообщению, в RegularOperations пропишем метод, похожий на showReplyMenu(). Только он будет добавлять к ответному сообщению встроенную клавиатуру

    /**
* Отобразить меню
*
* @param sendMessage ответное сообщение
*/
public void showInlineMenu(SendMessage sendMessage) {
// создаём разметку встроенной клавиатуры
InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();
// создаём кнопку добавления задачи
InlineKeyboardButton addTaskBtn = new InlineKeyboardButton();
addTaskBtn.setText(BTN_ADD_TASK_TEXT);
addTaskBtn.setCallbackData(BTN_ADD_TASK_CALLBACK_NAME);

// создаём кнопку списка задач
InlineKeyboardButton taskListBtn = new InlineKeyboardButton();
taskListBtn.setText(BTN_LIST_TASK_TEXT);
taskListBtn.setCallbackData(BTN_LIST_TASK_CALLBACK_NAME);
// создаём строки таблицы
List<InlineKeyboardButton> keyboardButtonsRow1 = new ArrayList<>();
List<InlineKeyboardButton> keyboardButtonsRow2 = new ArrayList<>();
keyboardButtonsRow1.add(addTaskBtn);
keyboardButtonsRow2.add(taskListBtn);
// добавляем строки в список строк
List<List<InlineKeyboardButton>> rowList = new ArrayList<>();
rowList.add(keyboardButtonsRow1);
rowList.add(keyboardButtonsRow2);
// сохраняем клавиатуру в разметку
inlineKeyboardMarkup.setKeyboard(rowList);
// сохраняем разметку
sendMessage.setReplyMarkup(inlineKeyboardMarkup);
}

Вызывать его будем для каждого вызова метода hello()

    /**
* Операция старта
*
* @param message - пришедшее сообщение
* @return ответ
*/
public String hello(Message message, SendMessage sendMessage) {
showInlineMenu(sendMessage);

Перезапустите сервер// highlight-end, и теперь на каждое авторизованное сообщение в ответное сообщение будет добавляться
встроенная клавиатура.


return "Привет, " + message.getFrom().getFirstName() + "!\n" +
"Вы написали:\n" + message.getText();
}

Перепишем теперь метод обработки веб-хуков в TelegramBot:

    /**
* Обработчик запросов от телеграм-сервера
*
* @param update - объект запроса
* @return - объект запроса API телеграма
*/
@Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) {
// Если в запросе есть `Callback`
if (update.hasCallbackQuery()) {
// получаем его объект
CallbackQuery callbackQuery = update.getCallbackQuery();
String data = callbackQuery.getData();
// формируем ответ
SendMessage sendMessage = new SendMessage();
// получаем сообщение
Message message = callbackQuery.getMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText(switch (data) {
case BTN_ADD_TASK_CALLBACK_NAME -> doWaitForTask();
case BTN_LIST_TASK_CALLBACK_NAME -> regularOperations.listTask(connectedUser);
default -> regularOperations.hello(message, sendMessage);
});
// возвращаем ответное сообщение
return sendMessage;
} else {
// получаем сообщение
Message message = update.getMessage();
// если у сообщения есть текст
if (message.hasText()) {
// формируем ответ
SendMessage sendMessage = new SendMessage();
// переходим в чат с пользователем
sendMessage.setChatId(String.valueOf(message.getChatId()));
// задаём текст сообщения
sendMessage.setText(switch (botState) {
case STATE_START -> processStart(message, sendMessage);
case STATE_WAIT_FOR_USERNAME -> waitForUsername(message);
case STATE_WAIT_FOR_PASSWORD -> waitForPassword(message, sendMessage);
case STATE_CONNECTED -> processConnected(message, sendMessage);
case STATE_WAIT_FOR_TASK -> waitForTask(message);
});
// возвращаем ответное сообщение
return sendMessage;
}
}
return null;
}

Перезапустите сервер, и теперь на каждое авторизованное сообщение в ответное сообщение будет добавляться встроенная клавиатура.

cmd

Если нажать на её кнопки, то они выполнятся заложенные нами команды.

cmd

Скачать исходники можно здесь

Самостоятельная работа

В качестве самостоятельной работы вам необходимо:

  • замените команды /addTask и /listTask на Добавить и Список
  • напишите регистрацию на сайте с помощью телеграм-бота
  • напишите удаление задачи по id, выводите его в скобках у названия
  • оставьте одну встроенную кнопку Добавить и добавляйте по ней текс, который пришёл в ответе

Свой бот

Когда выполните основную часть, переходите к творческой.

Идеи для своего бота можно почерпнуть в следующих статьях: