TL;DR: Если у вас есть Telegram-канал и вы устали руками заполнять отложку, то такой бот здорово облегчит вам жизнь.

Стандартный алгоритм создания отложенного поста выглядит так:
-
Открыть канал
-
Создать пост
-
Выбрать тип публикации «Отложенная»
-
Указать время поста
-
Отправить публикацию
При фиксированном интервале между постами алгоритм напрашивается на оптимизацию, в идеале хотелось бы оставить только пункты 1, 2, 5. Причем пункт 2 прокачать до «Создать посты».
Несмотря на обилие готовых решений, большинство их них перегружено функциями (зачастую платными) и работа с ними может, наоборот, увеличить время создания поста. Поэтому было решено реализовать собственного бота, которому можно просто отсылать фотографии (видео, документы, что угодно), а он бы сам уже добавлял их в отложенные публикации, основываясь на времени последнего поста.
Звучит здорово, но этот подход не сработал из-за того, что боту недоступно это самое время последнего поста в канале, а также из-за этого:

Получается без собственной реализации отложки не обойтись.
Идея
У любого файла, загруженного на сервера Telegram, есть уникальный fileId. Если мы отправим боту фотографию, то он сможет достать этот id из входящего сообщения и сохранить в базу:

Далее, когда настанет время, мы сможем использовать сохраненный fileId, чтобы отправить пост в канал.
Создаем проект
Бота будем писать на Java с использованием Spring Boot и библиотеки TelegramBots,. В качестве БД используем PostgreSQL. На Spring Initializr сгенерируем наш проект с необходимыми зависимостями:

Откроем сгенерированный проект в IDE. В build.gradle в тегdependencies добавим библиотеку для работы с ботами:
implementation 'org.telegram:telegrambots-spring-boot-starter:5.5.0'
Далее настроим подключение к нашей локальной БД. Для этого в application.yaml пропишем:
spring: datasource: url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/postgres}
И создадим класс конфигурации БД:
@Configuration public class DatabaseConfig { @Value("${spring.datasource.url}") private String dbUrl; @Bean public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl(dbUrl); return new HikariDataSource(config); } }
Создадим миграции:

databaseChangeLog: - changeSet: id: 1-add-record author: ananac changes: - createTable: tableName: record columns: - column: name: id type: bigint constraints: primaryKey: true nullable: false - column: name: file_id type: varchar(255) constraints: nullable: true - column: name: comment type: text constraints: nullable: true - column: name: data_type type: varchar(15) constraints: nullable: false - column: name: create_date_time type: timestamp constraints: nullable: false - column: name: post_date_time type: timestamp constraints: nullable: true - column: name: author type: varchar(255) constraints: nullable: false
databaseChangeLog: - include: file: db/changelog/1-add-record.yaml
После этого можно запускать приложение, чтобы миграции накатились и в БД появилась наша таблица для хранения постов.
Создаем бота
Идем к @BotFather, с помощью команды /newbot создаем нового бота и получаем API токен. Прописываем полученные данные в application.yaml, заодно укажем свой userId и chatId канала, в который мы будем постить. Все это можно узнать по адресу https://api.telegram.org/bot<вставить_токен_бота>/getUpdates. Там хранятся эвенты, такие как входящие сообщения, которые еще не были обработаны ботом.
telegram: name: botname token: 1793090787:AaaaAAAAAAAAAAAAAAAAAAAAAAAaaaaaaaa chatId: -1948372984327 adminId: 265765765
Пишем логику
Реализуем сущность для созданной нами таблицы:
@Entity @Table(name = "record") @Data @RequiredArgsConstructor public class Record { @Id private long id; private String fileId; private String comment; private String dataType; private LocalDateTime createDateTime; private LocalDateTime postDateTime; private String author; }
И JPA-репозиторий с необходимыми нам запросами:
@Repository public interface RecordRepository extends JpaRepository<Record, Long> { @Query("select r from Record r where r.createDateTime = (select min(r1.createDateTime) from Record r1 where r1.postDateTime = null)") Optional<Record> getFirstRecordInQueue(); @Query("select r from Record r where r.postDateTime = (select max(r1.postDateTime) from Record r1)") Optional<Record> getLastPostedRecord(); @Query("select count(*) from Record r where r.postDateTime = null") long getNumberOfScheduledPosts(); @Transactional @Modifying @Query("delete from Record r where r.postDateTime = null") void clear(); }
Займемся непосредственно обработчиком входящих сообщений. Создаем новый класс, отнаследованный от TelegramLongPollingBot. В нем определяем метод, который будет обрабатывать входящие события. Мы хотим, чтобы с ботом мог работать только пользователь указанный в конфиге, поэтому добавим проверку по userId:
@Component @Getter @RequiredArgsConstructor public class TelegramBotHandler extends TelegramLongPollingBot { private final RecordRepository recordRepository; @Value("${telegram.name}") private String name; @Value("${telegram.token}") private String token; @Value("${telegram.chatId}") private String chatId; @Value("${telegram.adminId}") private Set<Long> adminId; @Override public String getBotUsername() { return name; } @Override public String getBotToken() { return token; } @Override public void onUpdateReceived(Update update) { if (update.getMessage() != null) { Long userId = update.getMessage().getFrom().getId(); if (adminId.contains(userId)) { processMessage(update.getMessage()); } else { reply(userId, "Permission denied"); } } } private void reply(Long chatId, String text) { try { SendMessage sendMessage = new SendMessage(); sendMessage.setChatId(String.valueOf(chatId)); sendMessage.setText(text); execute(sendMessage); } catch (TelegramApiException e) { e.printStackTrace(); } } }
Далее реализуем метод сохранения поста в БД. Пока делаем поддержку только для фото, но в будущем ничего не мешает расшириться на все типы вложений. Помним, что во входящем сообщении лежит несколько файлов в разном разрешении, нас интересует только самый большой:
private void processMessage(Message message) { Long chatId = message.getChatId(); if (message.getPhoto() != null && !message.getPhoto().isEmpty()) { Record record = buildRecord(message); recordRepository.save(record); reply(chatId, "Добавлено. Количество постов в отложке: " + recordRepository.getNumberOfScheduledPosts()); } else { reply(chatId, "Принимаются только фото"); } } private Record buildRecord(Message message) { Record record = new Record(); String fileId = getLargestFileId(message); record.setFileId(fileId); record.setComment(message.getCaption()); record.setDataType("PHOTO"); record.setId(message.getMessageId()); record.setCreateDateTime(LocalDateTime.now()); record.setAuthor(message.getFrom().getUserName()); return record; } private String getLargestFileId(Message message) { return message.getPhoto().stream() .max(Comparator.comparing(PhotoSize::getFileSize)) .orElse(null) .getFileId(); }
Пост в базу мы добавили, перейдем к постингу. Создадим новый класс, внутри будет метод с аннотацией @Scheduled(fixedDelayString = "60000"), что означает, что он будет запускаться каждую минуту. Не забываем также повесить аннотацию @EnableScheduling на наш Application класс. Для интервала постинга в application.yaml укажем, например, 120 минут.
@Component @RequiredArgsConstructor public class RecordService { private final RecordRepository recordRepository; private final TelegramBotHandler botHandler; @Value("${schedule.postingInterval}") private long postingInterval; @Scheduled(fixedDelayString = "60000") private void run() { Optional<Record> recordToPost = recordRepository.getFirstRecordInQueue(); if (recordToPost.isPresent()) { Optional<Record> lastPostedRecordOptional = recordRepository.getLastPostedRecord(); if (lastPostedRecordOptional.isPresent()) { Record lastPostedRecord = lastPostedRecordOptional.get(); Duration duration = Duration.between(lastPostedRecord.getPostDateTime(), LocalDateTime.now()); if (duration.toMinutes() >= postingInterval) { Record record = recordToPost.get(); botHandler.sendPhoto(record); } } else { Record record = recordToPost.get(); botHandler.sendPhoto(record); } } } }
Метод запускается раз в минуту и первым делаем проверяет есть ли в БД неопубликованные посты. Если посты есть, то проверяется не прошло ли 120 минут с момента публикации последнего поста и на основании этого принимается решении о постинге. Также учитываем, что при первом запуске у нас не будет опубликованных постов в БД.
Далее добавим пару команд, чтобы с ботом было удобнее работать:

И реализуем их в коде. Команду для очистки сделаем с подтверждением, чтобы избежать мискликов:
private void processMessage(Message message) { Long chatId = message.getChatId(); if (message.getPhoto() != null && !message.getPhoto().isEmpty()) { Record record = buildRecord(message); recordRepository.save(record); reply(chatId, "Добавлено. Количество постов в отложке: " + recordRepository.getNumberOfScheduledPosts()); } else if (message.getText() != null) { switch (message.getText()) { case "/info": { reply(chatId, "Количество постов в отложке: " + recordRepository.getNumberOfScheduledPosts()); break; } case "/clear": { reply(chatId, "Чтобы очистить напиши /delete"); break; } case "/delete": { recordRepository.clear(); reply(chatId, "Очищено. Количество постов в отложке: " + recordRepository.getNumberOfScheduledPosts()); break; } default: { break; } } } else { reply(chatId, "Принимаются только фото"); } } public void sendPhoto(Record record) { try { SendPhoto sendPhoto = new SendPhoto(); sendPhoto.setChatId(chatId); sendPhoto.setPhoto(new InputFile(record.getFileId())); execute(sendPhoto); afterPost(record); } catch (TelegramApiException e) { e.printStackTrace(); } } private void afterPost(Record record) { record.setPostDateTime(LocalDateTime.now()); recordRepository.save(record); }
Запуск и проверка
Поднимаем приложение и проверяем:

В дальнейшем приложение можно задеплоить в облако, например, на Heroku (что и было сделано с этим ботом) по этому гайду. С кодом можно ознакомиться здесь.
ссылка на оригинал статьи https://habr.com/ru/post/596579/
Добавить комментарий