
В предыдущих сериях
Это третья статья в моей серии "для самых маленьких" - первая была посвящена "классическому" Telegram-боту, наследуемому от TelegramLongPollingBot, вторая - боту на вебхуках на Spring с блекджеком и ш БД Redis и клавиатурами.
Для кого написано
Если вы ни разу не писали Telegram-ботов на Java с использованием вебхуков и только начинаете разбираться — эта статья для вас. В ней подробно и с пояснениями описано создание реального бота, автоматизирующего одну очень простую функцию. Можно использовать статью как мануал для создания скелета своего бота, а потом подключить его к своей бизнес-логике.
Я пытаюсь писать как для себя, а не сразу для умных — надеюсь, кому-нибудь это поможет быстрее въехать в тему.
Предыстория
Давать доступ к возможностям продукта только покупателям подписки - нормально, это бизнес. Выводить раздражающую значительную часть пользователей фичу, а потом разрешать отказаться от неё только за деньги - поедание экскрементов.
Большинство преимуществ Telegram Premium не вызывают никаких вопросов, но запрет на отправку себе голосовых сообщений за деньги - это низко, Telegram.
К счастью, наш любимый мессенджер настолько хорош, что обойти эту несправедливость можно с помощью очень простого Voice4PremiumBot.
Что в статье есть, чего нет
В статье есть про:
создание бекенда Telegram-бота на вебхуках на Java 11 с использованием Spring;
отправку пользователю текстовых сообщений, изображений и аудио;
конвертацию файлов .ogg в .mp3;
удаление временных файлов по расписанию;
локальный запуск бота;
использование утилиты ngrok для локального дебага бота на вебхуках;
создание тестового метода для проверки работы приложения без использования Telegram для локализации проблемы при дебаге.
В статье нет про:
общение с BotFather (создание бота и получение его токена подробно и понятно описано во многих источниках, вот первый попавшийся мануал);
деплой - в предыдущей статье есть подробный порядок развёртывания на Heroku, повторяться не буду.
Исходный код лежит на GitHub. Если у вас вдруг есть вопросы, пишите в личку, с удовольствием проконсультирую.
Бизнес-функции бота
Бот позволяет:
выводить картинку-справку в ответ на команду /start;
конвертировать голосовые сообщения пользователя в файлы формата .mp3;
оповещать пользователя о неверном формате сообщения или возникшей ошибке.
Пользоваться просто - отправить боту голосовое сообщение, получить в ответ файл .mp3 с тем же аудио-содержимым, переслать пользователю Telegram Premium и наблюдать реакцию. Получатель не поймёт, что файл перенаправлен из бота - на файле отсутствует пометка "forwarded from ...". Уровень и длительность дальнейшего троллинга - на ваш вкус.
Можно потыкать - Voice4PremiumBot. Выглядит так:

Способы, которые не взлетели
Конечно, хотелось запилить бота совсем на скорую руку, без конвертации файлов, но Telegram последовательно не позволил сделать это. Не удалось:
получить от Telegram
fileIdи отправить его обратно, но как audio или document, а не voice - отправляет всё равно как voice;скачать файл .ogg (используя тот же
fileId) и отправить его обратно, но как audio или document, а не voice - отправляет всё равно как voice.
Делаем вывод, что Telegram воспринимает любой файл .ogg как голосовое сообщение - но только отправленный через API, поскольку через интерфейс .ogg можно отправить как файл, в том числе пользователям Telegram Premium.
Ну что ж, конвертировать так конвертировать.
Порядок разработки
разобраться с зависимостями;
создать бота;
обработать сообщения пользователя;
разобраться с конвертированием файлов;
научиться взаимодействовать с API Telegram;
локально запустить.
Ниже подробно расписан каждый пункт.
Зависимости
Для управления зависимостями используем Apache Maven. Нужные зависимости - собственно Telegram Spring Boot, Lombok и библиотека ffmpeg-cli-wrapper для конвертации аудио-файлов.
Создаём вот такой
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath/> </parent> <modelVersion>4.0.0</modelVersion> <groupId>ru.taksebe.telegram</groupId> <artifactId>premium-audio</artifactId> <version>1.0-SNAPSHOT</version> <name>premium-audio</name> <description>Накажи мажора с премиумом!</description> <packaging>jar</packaging> <properties> <java.version>11</java.version> <slf4j.version>1.7.30</slf4j.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> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.telegram</groupId> <artifactId>telegrambots-spring-boot-starter</artifactId> <version>5.3.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>compile</scope> </dependency> <dependency> <groupId>net.bramp.ffmpeg</groupId> <artifactId>ffmpeg</artifactId> <version>0.7.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> <configuration> <additionalProperties> <encoding.source>${project.build.sourceEncoding}</encoding.source> <encoding.reporting>${project.reporting.outputEncoding}</encoding.reporting> <java.source>${maven.compiler.source}</java.source> <java.target>${maven.compiler.target}</java.target> </additionalProperties> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Создаём бота
Нам понадобится файл настроек application - я предпочитаю делать его в формате .yaml, но если вам удобнее .properties - не суть:
application.yaml
telegram: api-url: "https://api.telegram.org/" bot-name: "Имя бота - от BotFather" bot-token: "Токен бота - от BotFather" webhook-path: "Адрес вебхука - локально получаем от ngrok" server: port: "для локального дебага через ngrok я использую 5000" files: incoming: "префикс названия временных файлов голосовых сообщений - нужен, чтобы найти потом эти временные файлы и удалить их" outgoing: "префикс названия временных файлов .mp3 - нужен, чтобы найти потом эти временные файлы и удалить их" ffmpeg: path: "путь до файла ffmpeg (если запускается под Linux) или ffmpeg.exe (если под Windows)" schedule: cron: delete-temp-files: 0 */10 * ? * * //крон для удаления временных файлов message: start: picture-file-id: "Telegram-идентификатор картинки, отправляемой пользователю в ответ на команду /start" text: "текст сообщения в ответ на команду /start" too-big-voice: text: "текст сообщения в ответ на отправку слишком длинного голосового сообщения (лимит - 10 минут)" illegal-message: text: "текст сообщения в ответ на отправку любого типа сообщений, кроме /start и голосовых" wtf: text: "текст сообщения в случае возникновения внутренней ошибки работы приложения"
Чтобы достать настройки, нужные для работы бота, создадим конфигурационный файл:
TelegramConfig.java
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 TelegramConfig { @Value("${telegram.webhook-path}") String webhookPath; @Value("${telegram.bot-name}") String botName; @Value("${telegram.bot-token}") String botToken; @Value("${message.too-big-voice.text}") String tooBigVoiceText; @Value("${message.illegal-message.text}") String illegalMessageText; @Value("${message.wtf.text}") String wtfText; }
Создадим класс для самого бота. Он получает сообщения, отсекает на всякий случае пустые и перенаправляет их в класс-обработчик. Кроме того, в случае возникновения ошибок обработки класс перехватывает исключения и в зависимости от их типа отправляет пользователю нужную текстовку из настроек:
WriteReadBot.java
import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.experimental.FieldDefaults; 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.Message; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.starter.SpringWebhookBot; import ru.taksebe.telegram.premium.exceptions.TooBigVoiceMessageException; import java.io.IOException; @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) public class WriteReadBot extends SpringWebhookBot { String botPath; String botUsername; String botToken; String tooBigVoiceText; String illegalMessageText; String wtfText; MessageHandler messageHandler; public WriteReadBot(SetWebhook setWebhook, MessageHandler messageHandler) { super(setWebhook); this.messageHandler = messageHandler; } @Override public BotApiMethod<?> onWebhookUpdateReceived(Update update) { try { return handleUpdate(update); } catch (TooBigVoiceMessageException e) { return new SendMessage(update.getMessage().getChatId().toString(), this.tooBigVoiceText); } catch (IllegalArgumentException e) { return new SendMessage(update.getMessage().getChatId().toString(), this.illegalMessageText); } catch (Exception e) { return new SendMessage(update.getMessage().getChatId().toString(), this.wtfText); } } private BotApiMethod<?> handleUpdate(Update update) throws IOException { if (update.hasCallbackQuery()) { return null; } else { Message message = update.getMessage(); if (message != null) { return messageHandler.answerMessage(message); } return null; } } }
Нам понадобится бин бота, и мы создадим его в ещё одном конфигурационном файле, используя настройки бота и вебхука:
SpringConfig.java
import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook; import ru.taksebe.telegram.premium.telegram.MessageHandler; import ru.taksebe.telegram.premium.telegram.WriteReadBot; @Configuration @AllArgsConstructor public class SpringConfig { private final TelegramConfig telegramConfig; @Bean public SetWebhook setWebhookInstance() { return SetWebhook.builder().url(telegramConfig.getWebhookPath()).build(); } @Bean public WriteReadBot springWebhookBot(SetWebhook setWebhook, MessageHandler messageHandler) { WriteReadBot bot = new WriteReadBot(setWebhook, messageHandler); bot.setBotPath(telegramConfig.getWebhookPath()); bot.setBotUsername(telegramConfig.getBotName()); bot.setBotToken(telegramConfig.getBotToken()); bot.setTooBigVoiceText(telegramConfig.getTooBigVoiceText()); bot.setIllegalMessageText(telegramConfig.getIllegalMessageText()); bot.setWtfText(telegramConfig.getWtfText()); return bot; } }
Используя бин бота, создаём контроллер:
WebhookController.java
import lombok.AllArgsConstructor; 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; import ru.taksebe.telegram.premium.telegram.WriteReadBot; @RestController @AllArgsConstructor public class WebhookController { private final WriteReadBot writeReadBot; @PostMapping("/premium") public BotApiMethod<?> onUpdateReceived(@RequestBody Update update) { return writeReadBot.onWebhookUpdateReceived(update); } }
И, наконец, нам нужно приложение, чтобы запустить всё это великолепие. Добавляем аннотацию EnableScheduling - она позволяет поддерживать работу по расписанию и понадобится нам для удаления временных файлов, об этом ниже:
PremiumAudioTelegramBotApplication.java
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling public class PremiumAudioTelegramBotApplication { public static void main(String[] args) { SpringApplication.run(PremiumAudioTelegramBotApplication.class, args); } }
Бот создан, но он не работает - никто не разбирает сообщения пользователя, не конвертирует аудио и ничего не отправляет в Telegram.
Разбираем сообщение пользователя
Пользователь может отправить боту всего два типа легальных сообщений - стандартную команду /start и голосовое сообщение. В ответ на первую бот отправляет инструкцию в виде картинки с текстом, а голосовухи отправляются в конвертер.
Для подготовки к конвертации необходимо:
проверить длительность голосового сообщения - чтобы не создавать повышенной нагрузки, сообщения длиной больше 10 минут не обрабатываются;
скачать файл голосовухи - в сообщении приходит только его идентификатор, который мы отправляем в
TelegramApiClientи получаем в ответ временный файл .ogg;создать временный файл .mp3 для отправки в конвертер - он "наполнит" его аудио из голосового сообщения.
После завершения конвертации файл .mp3 отправляется пользователю через API Telegram в виде массива байт, а хулиганства ради мы ещё и переопределяем метод получения названия файла, делая его максимально визуально похожим на интерфейс голосового сообщения в Telegram:
MessageHandler.java
import lombok.AccessLevel; import lombok.experimental.FieldDefaults; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Component; import org.telegram.telegrambots.meta.api.methods.BotApiMethod; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.Voice; import ru.taksebe.telegram.premium.exceptions.TooBigVoiceMessageException; import ru.taksebe.telegram.premium.utils.Converter; import java.io.File; import java.io.IOException; import java.nio.file.Files; @Component @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class MessageHandler { Converter converter; TelegramApiClient telegramApiClient; String tempFileNamePrefix; public MessageHandler(Converter converter, TelegramApiClient telegramApiClient, @Value("${files.outgoing}") String tempFileNamePrefix) { this.converter = converter; this.telegramApiClient = telegramApiClient; this.tempFileNamePrefix = tempFileNamePrefix; } public BotApiMethod<?> answerMessage(Message message) throws IOException { if (message.hasVoice()) { convertVoice(message); } else if (message.getText() != null && message.getText().equals("/start")) { telegramApiClient.uploadStartPhoto(message.getChatId().toString()); } else { throw new IllegalArgumentException(); } return null; } private void convertVoice(Message message) throws IOException { Voice voice = message.getVoice(); if (voice.getDuration() > 600) { throw new TooBigVoiceMessageException(); } File source = telegramApiClient.getVoiceFile(voice.getFileId()); File target = File.createTempFile(this.tempFileNamePrefix, ".mp3"); try { converter.convertOggToMp3(source.getAbsolutePath(), target.getAbsolutePath()); } catch (Exception e) { throw new IOException(); } telegramApiClient.uploadAudio(message.getChatId().toString(), new ByteArrayResource(Files.readAllBytes(target.toPath())) { @Override public String getFilename() { return "IlııIIIıııIııııııIIIIllıııııIıııııı.mp3"; } } ); } }
Конвертируем аудио
Конвертацию будет осуществлять ffmpeg - необходимо скачать нужную версию с официального сайта и положить в resources, чтобы наш класс-конвертер мог его найти.
Кстати, создадим его - он будет конвертировать один временный файл в другой, используя библиотеку ffmpeg-cli-wrapper и путь до файла ffmpeg из настроек:
Converter.java
import net.bramp.ffmpeg.FFmpeg; import net.bramp.ffmpeg.FFmpegExecutor; import net.bramp.ffmpeg.builder.FFmpegBuilder; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; @Component public class Converter { private final FFmpeg ffmpeg; public Converter(@Value("${ffmpeg.path}") String ffmpegPath) throws IOException { this.ffmpeg = new FFmpeg(new File(ffmpegPath).getPath()); } public void convertOggToMp3(String inputPath, String targetPath) throws IOException { FFmpegBuilder builder = new FFmpegBuilder() .setInput(inputPath) .overrideOutputFiles(true) .addOutput(targetPath) .setAudioCodec("libmp3lame") .setAudioBitRate(32768) .done(); FFmpegExecutor executor = new FFmpegExecutor(this.ffmpeg); executor.createJob(builder).run(); try { executor.createTwoPassJob(builder).run(); } catch (IllegalArgumentException ignored) {//отлавливаем и игнорируем ошибку, возникающую из-за отсутствия видеоряда (конвертер предназначен для видео) } } }
Общаемся с API Telegram
API Telegram нам нужно для работы с файлами:
отправлять пользователю стартовое сообщение в виде картинки с текстом (метод
uploadStartPhoto(String chatId)). Идентификатор картинки и текст - из настроек;скачивать голосовое сообщение во временный файл .ogg по его идентификатору (метод
getVoiceFile(String fileId)), присваивая нужный префикс в название для последующего удаления по расписанию;отправлять пользователю аудио в виде файла .mp3 (метод
uploadAudio(String chatId, ByteArrayResource value)).
Идентификатор картинки проще всего получить уже после первого запуска бота, направив ему нужное изображение - да, команда /start у вас в итоге упадёт, но перед этим под дебагом можно изучить объект Message и найти во вложенном списке photo в любом из трёх объектов поле fileId.
Получаем вот такого REST-клиента для общения с Telegram:
TelegramApiClient.java
import lombok.AccessLevel; import lombok.experimental.FieldDefaults; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StreamUtils; import org.springframework.web.client.RestTemplate; import org.telegram.telegrambots.meta.api.objects.ApiResponse; import ru.taksebe.telegram.premium.exceptions.TelegramFileNotFoundException; import ru.taksebe.telegram.premium.exceptions.TelegramFileUploadException; import java.io.File; import java.io.FileOutputStream; import java.text.MessageFormat; import java.util.Objects; @Service @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class TelegramApiClient { String URL; String botToken; String startMessagePhotoFileId; String startMessageText; String tempFileNamePrefix; RestTemplate restTemplate; public TelegramApiClient(@Value("${telegram.api-url}") String URL, @Value("${telegram.bot-token}") String botToken, @Value("${message.start.picture-file-id}") String startMessagePhotoFileId, @Value("${message.start.text}") String startMessageText, @Value("${files.incoming}") String tempFileNamePrefix) { this.URL = URL; this.botToken = botToken; this.tempFileNamePrefix = tempFileNamePrefix; this.startMessagePhotoFileId = startMessagePhotoFileId; this.startMessageText = startMessageText; this.restTemplate = new RestTemplate(); } public void uploadStartPhoto(String chatId) { LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); map.add("photo", this.startMessagePhotoFileId); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers); try { restTemplate.exchange( MessageFormat.format("{0}bot{1}/sendPhoto?chat_id={2}&caption={3}", URL, botToken, chatId, this.startMessageText), HttpMethod.POST, requestEntity, String.class); } catch (Exception e) { throw new TelegramFileUploadException(); } } public void uploadAudio(String chatId, ByteArrayResource value) { LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); map.add("audio", value); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers); try { restTemplate.exchange( MessageFormat.format("{0}bot{1}/sendAudio?chat_id={2}", URL, botToken, chatId), HttpMethod.POST, requestEntity, String.class); } catch (Exception e) { throw new TelegramFileUploadException(); } } public File getVoiceFile(String fileId) { try { return restTemplate.execute( Objects.requireNonNull(getVoiceTelegramFileUrl(fileId)), HttpMethod.GET, null, clientHttpResponse -> { File ret = File.createTempFile(this.tempFileNamePrefix, ".ogg"); StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret)); return ret; }); } catch (Exception e) { throw new TelegramFileNotFoundException(); } } private String getVoiceTelegramFileUrl(String fileId) { try { ResponseEntity<ApiResponse<org.telegram.telegrambots.meta.api.objects.File>> response = restTemplate.exchange( MessageFormat.format("{0}bot{1}/getFile?file_id={2}", URL, botToken, fileId), HttpMethod.GET, null, new ParameterizedTypeReference<ApiResponse<org.telegram.telegrambots.meta.api.objects.File>>() { } ); return Objects.requireNonNull(response.getBody()).getResult().getFileUrl(this.botToken); } catch (Exception e) { throw new TelegramFileNotFoundException(); } } }
Удаляем ненужные файлы
Побочный продукт нашего бота - временные файлы .ogg и .mp3, располагающиеся в специальной директории операционной системы. Конечно, они будут удалены операционкой, но происходит это довольно редко, а нам они не нужны сразу после отправки - так почему бы их не почистить?
Создадим класс, поддерживающий работу по расписанию - за это отвечают аннотации EnableAsync над классом и Scheduled над методом.
Алгоритм работы простой - мы просматриваем все файлы во временной директории, отбираем те, что содержат префиксы, которые мы ранее добавили в названия наших аудио-файлов, и удаляем, если они не заняты другой (то есть нашей же) программой.
Метод deleteTempFiles() запускается с периодичностью, определённой в cron-настройке в файле application.yaml, сейчас - раз в 10 минут.
FileScheduler.java
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; @EnableAsync @Component public class FileScheduler { Logger logger = LoggerFactory.getLogger(FileScheduler.class); private final String incomingTempFileNamePrefix; private final String outgoingTempFileNamePrefix; public FileScheduler(@Value("${files.incoming}") String incomingTempFileNamePrefix, @Value("${files.outgoing}") String outgoingTempFileNamePrefix) { this.incomingTempFileNamePrefix = incomingTempFileNamePrefix; this.outgoingTempFileNamePrefix = outgoingTempFileNamePrefix; } @Async @Scheduled(cron = "${schedule.cron.delete-temp-files}") public void deleteTempFiles() { for (String path : getToDeletePathList()) { try { Files.deleteIfExists(Path.of(path)); } catch (FileSystemException e) { logger.debug(e.getMessage()); } catch (IOException e) { logger.error(e.getMessage()); } } } private List<String> getToDeletePathList() { File dir = new File(System.getProperty("java.io.tmpdir")); List<String> tempFilePathList = new ArrayList<>(); for (File file : Objects.requireNonNull(dir.listFiles())){ if (file.isFile() && needToDelete(file.getName())) tempFilePathList.add(file.getAbsolutePath()); } return tempFilePathList; } private boolean needToDelete(String fileName) { return fileName.contains(this.incomingTempFileNamePrefix) || fileName.contains(this.outgoingTempFileNamePrefix); }
Создаём эндпоинт для тестирования
По опыту, дебаг Telegram-ботов становится проще и быстрее, если разделить его на два этапа - работоспособность приложения и внешние факторы.
Для этого создадим простейший REST-контроллер, возвращающий одну и ту же строку - если он работает, то приложение взлетело, и ошибку надо искать где-то в кишках взаимодействия с Telegram.
TestController.java
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @GetMapping("/premium/test") public String getTestMessage() { return "I believe I can fly"; } }
Запускаем локально
Нам нужен вебхук, и мы получим его, используя утилиту ngrok. Скачав и открыв его, отправляем команду ngrok http 5000 (или другой порт, если по каким-то причинам 5000 вам не нравится):

Получаем на 2 часа URL, который можем использовать как вебхук:

Вставляем его в applicatiom.yaml в настройку telegram.webhook-path, добавив в конце /premium (такой эндпоинт в нашем контроллере).
Регистрируем вебхук в Telegram, формируя в строке браузера запрос вида:
https://api.telegram.org/bot<токен бота>/setWebhook?url=<URL от ngrok>/premium
… видим ответ:
{"ok":true,"result":true,"description":"Webhook was set"}
… и запускаем приложение в своей IDE.
Благодарность
Лучшему иллюстратору, киноману и доброму другу desvvt за соавторство идеи и оформление.
