Экономим свое время. Ускоряем создание отложенных постов в Telegram с помощью бота

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Видеть это окно каждый раз при создании отложенного поста очень утомляет
Видеть это окно каждый раз при создании отложенного поста очень утомляет

Стандартный алгоритм создания отложенного поста выглядит так:

  1. Открыть канал

  2. Создать пост

  3. Выбрать тип публикации "Отложенная"

  4. Указать время поста

  5. Отправить публикацию

При фиксированном интервале между постами алгоритм напрашивается на оптимизацию, в идеале хотелось бы оставить только пункты 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/


Интересные статьи

Интересные статьи

Привет, Хабр.Довольно интересным направлением "прикладной статистики" и NLP (Natural Languages Processing а вовсе не то что многие сейчас подумали) является анализ текстов. Интересно, что...
Этот год перевернул все планы с ног на голову. Закрытые границы, локдаун, а потом и затянувшаяся у многих удаленка… К ноябрю, когда появились первые новости об отмен...
Привет, дорогие подписчики! Наверное вы уже знаете о том, что мы запустили новый курс «Компьютерное зрение», занятия по которому стартуют уже в ближайшие дни. В преддверии старта занятий подготов...
Введение Привет, Хабр! Вновь я с уже второй статьей, затрагивающей API Яндекс.Музыки. Дело запланированное и упоминалось в первой статье. Руки дошли, дело сделано. Сегодня я расскажу об интер...
По мотивам статьи Телеграмм-бот для системного администратора (статья не моя, я только прочитал) захотел поделиться опытом создания Telegram-бота на PowerShell для управления серверами прило...