Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В предыдущих сериях
Это третья статья в моей серии "для самых маленьких" - первая была посвящена «классическому» 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 за соавторство идеи и оформление.