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.
The standard algorithm for creating a deferred post looks like this:
Open channel
Create post
Select “Delayed” publication type
Specify fasting time
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:
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:
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
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:
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…