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

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

Комментарии (11)


  1. 0xC0CAC01A
    12.10.2022 18:58

    Вопрос в тему. Есть у Телеграма API, чтобы я мог подключиться к своему же аккаунту и получать сообщения, а также слать ответы скриптом на Питоне? (и нет, боты - это не то, про что я спрашиваю)

    Ну и, чтоб два раза не вставать, тот же вопрос про WhatsApp


    1. Revertis
      12.10.2022 19:20
      +1

      С WhatsApp точно не получится, так как ФБ банит за такое, удаляет репозитории и подаёт в суд на всех ботописателей.


      1. strelkan
        14.10.2022 08:42

        банит или не банит, а venombot уже несколько лет работает почти без нареканий


    1. taksebe Автор
      12.10.2022 20:06

      Есть либа самого Телеграма для создания любых клиентов - https://core.telegram.org/#tdlib-build-your-own-telegram

      И есть Питон-клиент для неё - https://github.com/alexander-akhmetov/python-telegram

      Сам не трогал, в Питоне ничего не понимаю, гугл + помощь друзей)


      1. taksebe Автор
        12.10.2022 20:11

        https://pytelegram.readthedocs.io/en/latest/ - вот ещё один Питон-клиент для tdlib



    1. litalen
      13.10.2022 09:13
      +1

      Есть клиенты на разных языках, ничем не уступающие по функционалу клиентам "для пользователя". Для питона например есть прекраснейший Pyrogram: https://docs.pyrogram.org/ (https://pypi.org/project/Pyrogram/)


  1. Mumlum
    12.10.2022 21:25
    +1

    А почему 10 минут? Что-то ломается на 11-й минуте?


    1. taksebe Автор
      13.10.2022 09:27

      Нет, на 11-й всё нормально:)

      С увеличением длительности растёт время конвертации - для получаса это примерно 20 секунд, для часа - минута с копейками. Сам ffmpeg не падает, но у Telegram, видимо, наступает тайм-аут, и он отправляет повторный запрос, конвертация начинается ещё раз, и поэтому пользователь получает сконвертированный файл несколько раз. И да, всё это без нагрузки, когда про бот знал только я.

      Так что решил не рисковать под нагрузкой, 10 минут за глаза хватит для 99+% потребностей.


  1. Enverest
    12.10.2022 21:41
    +1

    Прикрепи голосовалку кто как относится к этой фиче телеграмма :)


    1. taksebe Автор
      13.10.2022 09:30

      Да ну этот хайп)) К тому же, какая теперь разница, как ни относись - голосовуху отправить можно через бота))))