JavaRush/Java Blog/Random EN/Telegram bot - reminder via webHook in Java or say no to ...

Telegram bot - reminder via webHook in Java or say no to Google calendar! Part 1

Published in the Random EN group
members
My name is Vladimir. I am 43 years old. And if you, reader, are over 40, then yes, after 40 you can become a programmer if you like it. My work has nothing to do with programming at all, I am a project manager in the field of automation and all that. But I am planning to change my occupation. Oh, these new trends... change your field of activity every 5-7 years. So : The project turned out to be quite large, so some points will have to be omitted or talked about briefly, in the hope that the reader knows how to Google. The Internet is full of publications of telegram bots working on the principle of Long polling. And there are very few that work on the Webhook principle. What it is? Long polling - this means your application itself will poll the telegram server for messages at a certain frequency, slowly. Webhook - means the telegram server will instantly redirect messages to the server you specify. In our case, courtesy of the Heroku service. You can, of course, read more about all this and about the bot in general on the Telegram website - https://tlgrm.ru/docs/bots/api The bot interface looks like this: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 1 I consider this application precisely as a training project for the reason that when writing I learned more information from this bot than when training. Do you want to learn to program? Start writing code!!! But! There will be no detailed instructions on how to upload the application to github or how to create a database. There is plenty of this on the Internet and it is described in great detail; besides, it will be a very long read. The application will work as follows: Enter a description of the event, enter the date and time of the event, select the frequency (you can do it once, you can have a reminder every day at a certain time, you can have it once a month at a certain time, or once a year). Variations of notifications can be added endlessly; I have a lot of ideas. Next, the entered data is saved into the database (also deployed for free on Heroku, 10,000 rows are free) Then, once at the beginning of the day at 0:00 server time, Spring retrieves from the database all events based on the criteria that should fire on that day and sends them for execution at the specified time. ATTENTION!!! THIS PART OF THE PROGRAM IS EXPERIMENTAL! THERE IS AN IMPLEMENTATION THAT IS MORE SIMPLE AND TRUE! THIS WAS DONE SPECIFICALLY TO SEE HOW THE TIME CLASS WORKS! You can touch the working bot with your own hands by typing @calendar_event_bot in the cart, but don’t count on it, because I’m still making fun of it. code - https://github.com/papoff8295/webHookBotForHabr Basically, to launch your own you need to take the following steps: • Register with @BotFather , it’s not difficult, get a token and name • Fork the project on github • Register on Heroku, create an application (we will go through it step by step), deploy from your repository. • Create a database on Heroku • Replace the corresponding fields in the repository with your own (token, name of tables in entities, webHookPath, user name, password and path to the database, this will all be parsed) • Make Heroku work 24/7 using https:/ /uptimerobot.com/ The final structure of the project is as follows: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 2 Let's start by creating a project in https://start.spring.io Select the dependencies we need as shown in the figure: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 3Select our own name for the project and click Generate . Next you will be prompted to save the project to your disk. All that remains is to open the pom.xm l file from your development environment. There is a finished project in front of you. Now we just need to add our main library. I used the library from https://github.com/rubenlagus/TelegramBots In general, you can get confused and do without it. After all, the whole point of the work is to concatenate a URL like this: https://api.telegram.org/bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/setWebhook?url=https://e9c658b548aa.ngrok.io Let’s look at it a little: https://api.telegram.org – telegram server. bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/ - after the word bot is a secret token that you receive when registering a bot. setWebhook?url=https://e9c658b548aa.ngrok.io – name of the method and its parameters. In this case, we install your webhook server, all messages will be sent to it. In general, I decided that the project was not too small for publication, but with manual implementation it would be generally unreadable. So, the final look of the pom file is:
<!--?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelversion>4.0.0</modelversion>
   <parent>
      <groupid>org.springframework.boot</groupid>
      <artifactid>spring-boot-starter-parent</artifactid>
      <version>2.5.0</version>
      <relativepath> <!-- lookup parent from repository -->
   </relativepath></parent>
   <groupid>ru.popov</groupid>
   <artifactid>telegrambot</artifactid>
   <version>0.0.1-SNAPSHOT</version>
   <name>telegrambot</name>
   <description>Demo project for Spring Boot</description>
   <properties>
      <java.version>1.8</java.version>
   </properties>
   <dependencies>
      <dependency>
         <groupid>org.springframework.boot</groupid>
         <artifactid>spring-boot-starter-web</artifactid>
      </dependency>

      <dependency>
         <groupid>org.springframework.boot</groupid>
         <artifactid>spring-boot-starter-data-jpa</artifactid>
      </dependency>

      <dependency>
         <groupid>org.springframework.boot</groupid>
         <artifactid>spring-boot-starter-test</artifactid>
         <scope>test</scope>
      </dependency>

      <!-- https://mvnrepository.com/artifact/org.telegram/telegrambots-spring-boot-starter -->
      <dependency>
         <groupid>org.telegram</groupid>
         <artifactid>telegrambots-spring-boot-starter</artifactid>
         <version>5.2.0</version>
      </dependency>

      <dependency>
         <groupid>org.projectlombok</groupid>
         <artifactid>lombok</artifactid>
         <version>1.18.16</version>
      </dependency>

      <dependency>
         <groupid>org.postgresql</groupid>
         <artifactid>postgresql</artifactid>
         <scope>runtime</scope>
      </dependency>

   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-maven-plugin</artifactid>
         </plugin>
      </plugins>
   </build>

</project>
Everything is ready to write our bot. Let's create the TelegramBot class . I won’t write the names of the folders, you can look at them in the project structure above.
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBot extends SpringWebhookBot {
    String botPath;
    String botUsername;
    String botToken;

    private TelegramFacade telegramFacade;

    public TelegramBot(TelegramFacade telegramFacade, DefaultBotOptions options, SetWebhook setWebhook) {
        super(options, setWebhook);
        this.telegramFacade = telegramFacade;
    }
    public TelegramBot(TelegramFacade telegramFacade, SetWebhook setWebhook) {
        super(setWebhook);
        this.telegramFacade = telegramFacade;
    }

    @Override
    public BotApiMethod<!--?--> onWebhookUpdateReceived(Update update) {
        return telegramFacade.handleUpdate(update);
    }
}
The class extends SpringWebhookBot from our telegram library, and we only need to implement one method, onWebhookUpdateReceived . It accepts parsed JSON as an Update object and returns what the telegram server wants to “hear” from us. Here we have annotations from the Lombok library . Lombok – making a programmer's life easier!! Well, that is. we don’t need to redefine getters and setters, Lombok does this for us, and we also don’t need to write an access level identifier. It’s no longer worth writing that this is done by the annotations @Getter, @Setter, @FieldDefaults The botPath field means our webhook address, which we will receive on Heroku later. The botUsername field means the name of our bot, which we will receive when registering our bot in Telegram. The botToken field is our token, which we will receive when registering our bot in Telegram. The telegramFacade field is our class where message processing will take place, we will return to it a little later, let it be red for now. Now it’s time for us to contact @BotFather and get the coveted botToken and botUsername. Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 4Just write to him on telegram and he will tell you everything. We write the data to our application.properties, in the end it will look like this:
server#server.port=5000

telegrambot.userName=@calendar_event_bot
telegrambot.botToken=1731265488:AAFDjUSk3vu5SFfgdfh556gOOFmuml7SqEjwrmnEF5Ak
#telegrambot.webHookPath=https://telegrambotsimpl.herokuapp.com/
telegrambot.webHookPath=https://f5d6beeb7b93.ngrok.io


telegrambot.adminId=39376213

eventservice.period =600000

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/telegramUsers
spring.datasource.username=postgres
spring.datasource.password=password

#spring.datasource.url=jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um126le5notq?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
#spring.datasource.username=ulmbeywyhvsxa
#spring.datasource.password=4c7646c69dbbgeacb98fa96e8daa6d9b1bl4894e67f3f3ddd6a27fe7b0537fd
This configuration is configured to work with a local database; later we will make the necessary changes. Replace botToken and username with your own. It is not good to use data from application.properties directly in the application. Let's create a bean or a wrapper class from this data.
@Component
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)

public class TelegramBotConfig {
    @Value("${telegrambot.webHookPath}")
    String webHookPath;
    @Value("${telegrambot.userName}")
    String userName;
    @Value("${telegrambot.botToken}")
    String botToken;
Here the @Value annotation initializes the corresponding lines from the application.properties file, which Spring knows about by default. And the @Component annotation creates a Bean for us when the application starts. Let's now look at the Spring configuration file:
@Configuration
public class AppConfig {
    private final TelegramBotConfig botConfig;

    public AppConfig(TelegramBotConfig botConfig) {
        this.botConfig = botConfig;
    }

    @Bean
    public SetWebhook setWebhookInstance() {
        return SetWebhook.builder().url(botConfig.getWebHookPath()).build();
    }

    @Bean
    public TelegramBot springWebhookBot(SetWebhook setWebhook, TelegramFacade telegramFacade) {
        TelegramBot bot = new TelegramBot(telegramFacade, setWebhook);
        bot.setBotToken(botConfig.getBotToken());
        bot.setBotUsername(botConfig.getUserName());
        bot.setBotPath(botConfig.getWebHookPath());

        return bot;
    }
}
There is no magic here; at startup, Spring creates SetWebhook and TelegramBot objects for us. Let's now create entry points for our messages:
@RestController
public class WebhookController {

    private final TelegramBot telegramBot;

    public WebhookController(TelegramBot telegramBot) {
        this.telegramBot = telegramBot;
    }

// point for message
    @PostMapping("/")
    public BotApiMethod<!--?--> onUpdateReceived(@RequestBody Update update) {
        return telegramBot.onWebhookUpdateReceived(update);
    }

    @GetMapping
    public ResponseEntity get() {
        return ResponseEntity.ok().build();
    }
}
The Telegram server sends messages in JSON format to the registered webhook address using the POST method, our controller receives them and transmits them to the telegram library in the form of an Update object. I did the get method just like that) Now we just need to implement some logic for processing messages and responses in the TelegramFacade class , I will give its short code so that you can launch the application and then go your own way or switch to deploy on Heroku, then it will be full version:
@Component
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramFacade {

    public BotApiMethod<!--?--> handleUpdate(Update update) {

        if (update.hasCallbackQuery()) {
            CallbackQuery callbackQuery = update.getCallbackQuery();
            return null;
        } else {

            Message message = update.getMessage();
            SendMessage sendMessage = new SendMessage();
            sendMessage.setChatId(String.valueOf(message.getChatId()));
            if (message.hasText()) {
                sendMessage.setText("Hello world");
                return sendMessage;
            }
        }
        return null;
    }

}
This method will respond to any Hello world! In order to launch our application, we just need to make sure that we can test our application directly from IDEA. To do this, we need to download the ngrok utility. https://ngrok.com/download This utility is a command line that gives us a temporary address for 2 hours and redirects all messages to the specified port of the local server. We launch and write ngrok http 5000 in the line (or you can specify your port): Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 5We get the result: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 6https://23b1a54ccbbd.ngrok.io - this is our webhook address. As you may have noticed in the properties file we wrote server.port=5000 when starting the tomcat server, it will occupy port 5000, make sure that these fields are the same. Also, do not forget that the address is given for two hours. On the command line, this is monitored by the Session Expires field. When the time runs out, you will need to get the address again and go through the procedure of registering it on telegram. Now we take the line https://api.telegram.org/bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/setWebhook?url=https://e9c658b548aa.ngrok.io And with deft hand movements we replace the token with ours, the url with ours, paste the resulting line into the browser and click enter. You should get the following result: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 7That's it, now you can run the application: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 8Check that your class with the main method was like this:
@SpringBootApplication
public class TelegramBotApplication {

   public static void main(String[] args) {
      SpringApplication.run(TelegramBotApplication.class, args);
   }
}
If you did everything correctly, now your bot will respond to any message with the phrase “ Hello world” . Then you can go your own way. If you are with me and you are interested in going through all the steps, then let’s start writing entities for the database and create the database itself. Let's start with the database: As I already said, I assume that you already have minimal skills in working with the database, and you have a local postgreSQL database installed , if not, follow this path. https://www.postgresql.org/download/ In the application.properties file, replace the database login and password with your own. In IDEA there is a database tab on the right, in it you need to click on +/Data source/PostgreSQL . As a result, when you click on Test Connection you should get a satisfactory result: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 9Now you can create a database with tables directly from the development environment, or you can use the pgadmin web interface , which is located in the start menu. We will need 3 tables:
CREATE TABLE users
(
    id               INTEGER PRIMARY KEY UNIQUE NOT NULL,
    name             VARCHAR,
    time_zone        INTEGER DEFAULT 0,
    on_off           BOOLEAN DEFAULT true
);

CREATE TABLE user_events
(
    user_id INTEGER ,
    time timestamp ,
    description varchar ,
    event_id serial,
    event_freq varchar default 'TIME',
    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

CREATE TABLE event_cash
(
    time timestamp ,
    description varchar ,
    user_id INTEGER ,
    id serial
);
I recommend creating this script separately; we will need it to create a database on Heroku, so as not to write it twice. Let's walk a little. I’ll say right away that we only need the event_cash table to work with Heroku due to its specifics and my insatiable desire to work with the Time class ; it will not be useful in the local version. In the users table we will record the id of the telegram user’s account, his name, which may not exist, the user’s time zone will be calculated for the correct sending of notifications, as well as the on/off status of sending notifications. We will record the user id , notification time, description in the user_events table , automatically generate an id for the event, and set the frequency of notifications. The event_cash table will record the notification before it is sent and, if sent, remove it from the table. The tables are ready, let's now add the entities.
@Entity
@Table(name = "user_events")
@Getter
@Setter
public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column( name = "event_id", columnDefinition = "serial")
    private int eventId;

    @Column(name = "time")
    @NotNull(message = "Need date!")
    private Date date;

    @Column(name = "description")
    @Size(min = 4, max = 200, message = "Description must be between 0 and 200 chars!")
    private String description;

    @Column(name = "event_freq", columnDefinition = "TIME")
    @Enumerated(EnumType.STRING)
    private EventFreq freq;

    @ManyToOne(fetch=FetchType.EAGER)
    @JoinColumn(name="user_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private User user;

    public Event() {
    }

    public Event(int eventId,
                 @NotNull(message = "Need date!") Date date,
                 @Size(min = 4, max = 200, message = "Description must be between 0 and 200 chars!")
                         String description,
                 EventFreq freq, User user) {
        this.eventId = eventId;
        this.date = date;
        this.description = description;
        this.freq = freq;
        this.user = user;
    }
}
@Entity
@Table(name = "users")
@Getter
@Setter
public class User {

    @Id
    @Column(name = "id")
    private long id;

    @Column(name = "name")
    private String name;

    @Column(name = "time_zone", columnDefinition = "default 0")
    //sets the broadcast time of events for your time zone
    private int timeZone;

    @OneToMany(mappedBy="user")
    private List<event> events;

    @Column(name = "on_off")
    // on/off send event
    private boolean on;

    public User() {
    }
}

</event>
@Entity
@Table(name = "event_cash")
@Getter
@Setter
//serves to save unhandled events after rebooting heroku
public class EventCashEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column( name = "id", columnDefinition = "serial")
    private long id;

    @Column(name = "time")
    private Date date;

    @Column(name = "description")
    private String description;

    @Column(name = "user_id")
    private long userId;

    public EventCashEntity() {
    }

    public static EventCashEntity eventTo(Date date, String description, long userId) {
        EventCashEntity eventCashEntity = new EventCashEntity();
        eventCashEntity.setDate(date);
        eventCashEntity.setDescription(description);
        eventCashEntity.setUserId(userId);
        return eventCashEntity;
    }
}
Let's go over the main points a little. @Entity – marks the class for our dada jpa that this class is an entity for the database, i.e. when retrieving data from the database, it will be presented to us in the form of an Event, User and EventCashEntity object. @Table – we say the name of our table in the database. To ensure that the table name is not underlined in red, we need to agree in IDEA with the proposed error correction option and click Assign data sources. And select our base there. @id and @GeneratedValue - used by Spring to create a database if it doesn't already exist. @Column is used to indicate the name of the fields in the table if they do not match, but the rules of good code recommend that you always write this. OneToMany attitude - I recommend spending time and figuring out what it is here https://en.wikibooks.org/wiki/Java_Persistence I can’t explain it more clearly, just believe me. Let me just say that in this case the @OneToMany annotation says that one user can have many events, and they will be provided to us in the form of a list. Now we need to get data from the tables. In the SRING DATA JPA library everything is already written for us, we just need to create an interface for each table and extend it from JpaRepository.
public interface EventRepository extends JpaRepository<event, long=""> {
    Event findByEventId(long id);
}
public interface UserRepository extends JpaRepository<user, long=""> {

    User findById(long id);
}
public interface EventCashRepository extends JpaRepository<eventcashentity, long=""> {
    EventCashEntity findById(long id);
}

</eventcashentity,></user,></event,>
The main logic for working with the database is transferred to a service, the so-called Data Access Object (DAO):
@Service
public class UserDAO {

    private final UserRepository userRepository;

    @Autowired
    public UserDAO(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findByUserId(long id) {
        return userRepository.findById(id);
    }

    public List<user> findAllUsers() {
        return userRepository.findAll();
    }

    public void removeUser(User user) {
        userRepository.delete(user);
    }


    public void save(User user) {
        userRepository.save(user);
    }

    public boolean isExist(long id) {
        User user = findByUserId(id);
        return user != null;
    }
}
@Service
public class EventDAO {

    private final UserRepository userRepository;
    private final EventRepository eventRepository;

    @Autowired
    public EventDAO(UserRepository userRepository, EventRepository eventRepository) {
        this.userRepository = userRepository;
        this.eventRepository = eventRepository;
    }

    public List<event> findByUserId(long userId) {
        User user = userRepository.findById(userId);
        return user.getEvents();
    }
    public List<event> findAllEvent() {
       return eventRepository.findAll();
    }

    public Event findByEventId(long eventId) {
        return eventRepository.findByEventId(eventId);
    }

    public void remove(Event event) {
        eventRepository.delete(event);
    }

    public void save(Event event) {
        eventRepository.save(event);
    }
}

</event></event></user>
@Service
//handles events not dispatched after reboot heroku
public class EventCashDAO {

    private EventCashRepository eventCashRepository;

    @Autowired
    public void setEventCashRepository(EventCashRepository eventCashRepository) {
        this.eventCashRepository = eventCashRepository;
    }

    public List<eventcashentity> findAllEventCash() {
        return eventCashRepository.findAll();
    }

    public void save(EventCashEntity eventCashEntity) {
        eventCashRepository.save(eventCashEntity);
    }

    public void delete(long id) {
        eventCashRepository.deleteById(id);
    }
}

</eventcashentity>
In this case, we do not have any data processing, we simply retrieve data from the tables. We are all ready to provide the complete code of the T elegramFacade class and begin to analyze the logic.
@Component
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramFacade {

    final MessageHandler messageHandler;
    final CallbackQueryHandler callbackQueryHandler;
    final BotStateCash botStateCash;

    @Value("${telegrambot.adminId}")
    int adminId;


    public TelegramFacade(MessageHandler messageHandler, CallbackQueryHandler callbackQueryHandler, BotStateCash botStateCash) {
        this.messageHandler = messageHandler;
        this.callbackQueryHandler = callbackQueryHandler;
        this.botStateCash = botStateCash;
    }

    public BotApiMethod<!--?--> handleUpdate(Update update) {

        if (update.hasCallbackQuery()) {
            CallbackQuery callbackQuery = update.getCallbackQuery();
            return callbackQueryHandler.processCallbackQuery(callbackQuery);
        } else {

            Message message = update.getMessage();
            if (message != null && message.hasText()) {
                return handleInputMessage(message);
            }
        }
        return null;
    }

    private BotApiMethod<!--?--> handleInputMessage(Message message) {
        BotState botState;
        String inputMsg = message.getText();
        //we process messages of the main menu and any other messages
        //set state
        switch (inputMsg) {
            case "/start":
                botState = BotState.START;
                break;
            case "Мои напоминания":
                botState = BotState.MYEVENTS;
                break;
            case "Создать напоминание":
                botState = BotState.CREATE;
                break;
            case "Отключить напоминания":
            case "Включить напоминания":
                botState = BotState.ONEVENT;
                break;
            case "All users":
                if (message.getFrom().getId() == adminId)
                botState = BotState.ALLUSERS;
                else botState = BotState.START;
                break;
            case "All events":
                if (message.getFrom().getId() == adminId)
                botState = BotState.ALLEVENTS;
                else botState = BotState.START;
                break;
            default:
                botState = botStateCash.getBotStateMap().get(message.getFrom().getId()) == null?
                        BotState.START: botStateCash.getBotStateMap().get(message.getFrom().getId());
        }
        //we pass the corresponding state to the handler
        //the corresponding method will be called
        return messageHandler.handle(message, botState);

    }
}
Let's look at what fields are needed for
final MessageHandler messageHandler;
    final CallbackQueryHandler callbackQueryHandler;
    final BotStateCash botStateCash;
If we all code in one class, then we will end up with a footcloth to the moon; therefore, we assign the logic for working with text messages to the MessageHandler class , and the logic for working with callbackquery messages to the CallbackQueryHandler class . It's time to figure out what allbackquery is and what types of menus there are. To do this, I’ll give you another picture of the bot’s interface: Telegram bot - reminder via webHook in Java or say no to Google calendar!  - 10There are two types of menus. Those that are attached to the bottom of the window - the main menu, and those that are assigned to the message, for example, the delete, edit, change belt buttons. When you click on the main menu button, a message of the same name is sent, for example, when you click “My Reminders” , the text “My Reminders” will simply be sent . And when installing the callbackquery keyboard, a specific value is set for each button and its value will be sent without displaying it on the screen. Next we have the BotStateCash field . This is a specially created class that will monitor the state of the bot, and attention, this is a complex element, you need to strain. The number of characters was exceeded, which, by the way, is not written anywhere)). So here's the link to part two
Comments
  • Popular
  • New
  • Old
You must be signed in to leave a comment
This page doesn't have any comments yet