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

7e6d44b8c3aac702025a059befc13cd8.jpg

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

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

0a0f322c3016548f8f52bccc5be18e53.jpg

Способы, которые не взлетели

Конечно, хотелось запилить бота совсем на скорую руку, без конвертации файлов, но 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



	
		org.springframework.boot
		spring-boot-starter-parent
		2.2.0.RELEASE
		
	
	4.0.0

	ru.taksebe.telegram
	premium-audio
	1.0-SNAPSHOT
	premium-audio
	Накажи мажора с премиумом!
	jar

	
		11
		1.7.30
		${java.version}
		${java.version}
		UTF-8
		UTF-8
	

	
		
			org.springframework.boot
			spring-boot-starter-web
		
		
			org.telegram
			telegrambots-spring-boot-starter
			5.3.0
		
		
			org.projectlombok
			lombok
			1.18.20
			compile
		
		
			net.bramp.ffmpeg
			ffmpeg
			0.7.0
		
	

	
		
			
				org.springframework.boot
				spring-boot-maven-plugin
				
					
						
							build-info
						
						
							
								${project.build.sourceEncoding}
								${project.reporting.outputEncoding}
								${maven.compiler.source}
								${maven.compiler.target}
							
						
					
				
			
		
	

Создаём бота

Нам понадобится файл настроек 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 map = new LinkedMultiValueMap<>();
        map.add("photo", this.startMessagePhotoFileId);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        HttpEntity> 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 map = new LinkedMultiValueMap<>();
        map.add("audio", value);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        HttpEntity> 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> response = restTemplate.exchange(
                    MessageFormat.format("{0}bot{1}/getFile?file_id={2}", URL, botToken, fileId),
                    HttpMethod.GET,
                    null,
                    new ParameterizedTypeReference>() {
                    }
            );
            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 getToDeletePathList() {
        File dir = new File(System.getProperty("java.io.tmpdir"));

        List 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 вам не нравится):

16347e3b5b469fd19f645e7fe7ba672d.png

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

55050784ac3879de9893588f685e5ae2.png

Вставляем его в applicatiom.yaml в настройку telegram.webhook-path, добавив в конце /premium (такой эндпоинт в нашем контроллере).

Регистрируем вебхук в Telegram, формируя в строке браузера запрос вида:

https://api.telegram.org/bot<токен бота>/setWebhook?url=/premium
… видим ответ:

{"ok":true,"result":true,"description":"Webhook was set"}
… и запускаем приложение в своей IDE.

Благодарность

Лучшему иллюстратору, киноману и доброму другу desvvt за соавторство идеи и оформление.

© Habrahabr.ru