We save our time. Speed ​​up the creation of deferred posts in Telegram using a bot

TL; DR: If you have a Telegram channel and you are tired of filling the deposit with your hands, then such a bot will greatly facilitate your life.

Seeing this window every time you create a postponed post is very tiring.
Seeing this window every time you create a postponed post is very tiring.

The standard algorithm for creating a deferred post looks like this:

  1. Open channel

  2. Create post

  3. Select “Delayed” publication type

  4. Specify fasting time

  5. Send publication

With a fixed interval between posts, the algorithm begs for optimization, ideally I would like to leave only points 1, 2, 5. Moreover, point 2 should be pumped to “Create posts”.

Despite the abundance of ready-made solutions, most of them are overloaded with functions (often paid) and working with them can, on the contrary, increase the post creation time. Therefore, it was decided to implement our own bot, to which you can simply send photos (videos, documents, whatever), and he himself would already add them to deferred publications, based on the time of the last post.

Sounds great, but this approach did not work due to the fact that the bot is not available this very time of the last post in the channel, and also because of this:

Durov, why?
Durov, why?

It turns out that you cannot do without your own implementation of pending.

Idea

Any file uploaded to Telegram servers has a unique fileId. If we send a photo to the bot, then it will be able to get this id from the incoming message and save it to the database:

In the case of a picture, several files are created on servers in different resolutions
In the case of a picture, several files in different resolutions are created on servers

Then, when the time is right, we can use the saved fileId to send the post to the channel.

We create a project

We will write bots in Java using Spring Boot and the library TelegramBots,. We use PostgreSQL as a database. On the Spring initializr Let’s generate our project with the required dependencies:

Let’s open the generated project in the IDE. V build.gradle in tagdependencies add a library for working with bots:

implementation 'org.telegram:telegrambots-spring-boot-starter:5.5.0'

Next, let’s set up a connection to our local database. For this in application.yaml let’s write down:

spring:
  datasource:
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/postgres}

And create a DB configuration class:

@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);
    }
}

Let’s create migrations:

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

After that, you can run the application so that migrations roll over and our table for storing posts appears in the database.

Create a bot

Go to @BotFather using the command /newbot create a new bot and get an API token. We register the received data in application.yaml, at the same time we will indicate our userId and chatId of the channel to which we will post. All this can be found at https://api.telegram.org/bot/getUpdates… Events are stored there, such as incoming messages that have not yet been processed by the bot.

telegram:
  name: botname
  token: 1793090787:AaaaAAAAAAAAAAAAAAAAAAAAAAAaaaaaaaa
  chatId: -1948372984327
  adminId: 265765765

Writing logic

Let’s implement the entity for the table we created:

@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;
}

And the JPA repository with the requests we need:

@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();

}

Let’s deal directly with the handler of incoming messages. Create a new class inherited from TelegramLongPollingBot… In it, we define a method that will handle incoming events. We want only the user specified in the config to work with the bot, so let’s add a check by 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();
        }
    }
}

Next, we will implement the method of saving the post to the database. So far, we are doing support only for photos, but in the future nothing prevents us from expanding to all types of attachments. Remember that the incoming message contains several files with different resolutions, we are only interested in the largest one:

    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();
    }

We added a post to the database, let’s move on to posting. Let’s create a new class, inside there will be a method with annotation @Scheduled(fixedDelayString = "60000")which means it will run every minute. Don’t forget to add the annotation @EnableScheduling to our Application class. For posting interval in application.yaml let’s indicate, for example, 120 minutes.

@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);
            }
        }
    }
}

The method is run once a minute and the first thing we do is to check if there are unpublished posts in the database. If there are posts, then it is checked whether 120 minutes have passed since the publication of the last post and on the basis of this a decision on posting is made. We also take into account that at the first launch, we will not have any published posts in the database.

Next, let’s add a couple of commands to make it more convenient to work with the bot:

And we will implement them in the code. We will make the command for cleaning with confirmation to avoid misclips:

    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);
    }

Launch and check

We raise the application and check:

Posts are removed and added
Posts are removed and added

In the future, the application can be deployed to the cloud, for example, on Heroku (which was done with this bot) by this guide… The code can be found here

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *