Выкуси, Telegram Premium — бот-конвертер голосовых сообщений для обхода ограничений (Java, Spring, вебхуки, ffmpeg)

от автора

В предыдущих сериях

Это третья статья в моей серии «для самых маленьких» — первая была посвящена «классическому» 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 за соавторство идеи и оформление.


ссылка на оригинал статьи https://habr.com/ru/post/692826/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *