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

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

Published in the Random EN group
The second part of the project - here is a link to the first: And so the BotState class : In order for our bot to understand what is expected of it at a certain point in time, for example, deleting a reminder, we need to somehow let our bot know that the numbers are entered and sent now should be treated as a reminder ID from the list and should be removed. Therefore, after clicking on the “Delete” button , the bot goes into the BotState.ENTERNUMBEREVENT state , this is a specially created Enum class with bot states.
public enum BotState {
    ENTERDESCRIPTION,//the bot will wait for the description to be entered.
    START,
    MYEVENTS, //the bot show to user list events.
    ENTERNUMBEREVENT,//the bot will wait for the number of event to be entered.
    ENTERDATE, //the bot will wait for the date to be entered
    CREATE, //the bot run created event
    ENTERNUMBERFOREDIT, //the bot will wait for the number of event to be entered
    EDITDATE, //the bot will wait for the date to be entered
    EDITDESCRIPTION,//the bot will wait for the description to be entered
    EDITFREQ,//the bot will wait callbackquery
    ALLUSERS, // show all users
    ALLEVENTS, //show all events
    ENTERNUMBERUSER,//the bot will wait for the number of user to be entered.
    ENTERTIME,//the bot will wait for the hour to be entered.
    ONEVENT // state toggle
}
And now we are expected to enter numbers - “ Enter the reminder number from the list .” After entering which they will go to the desired method for processing. Here is our state switch:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

    public void saveBotState(long userId, BotState botState) {
        botStateMap.put(userId, botState);
    }
}

</long,>
A regular map with a user ID and its status. The int adminId field is for me) Next, the logic of the handleUpdate method will check what kind of message this is? Callbackquery or just text? If this is regular text, then we go to the handleInputMessage method , where we process the main menu buttons, and if they were clicked, then we set the desired state, but if they weren’t clicked and this is unfamiliar text, then we set the state from the cache, if it is not there, then we set the starting state. Then the text goes into processing the handle method with the state we need. Now we present the logic of the MessageHandler class , which is responsible for processing messages depending on the state of the bot:
public class MessageHandler {

    private final UserDAO userDAO;
    private final MenuService menuService;
    private final EventHandler eventHandler;
    private final BotStateCash botStateCash;
    private final EventCash eventCash;

    public MessageHandler(UserDAO userDAO, MenuService menuService, EventHandler eventHandler, BotStateCash botStateCash, EventCash eventCash) {
        this.userDAO = userDAO;
        this.menuService = menuService;
        this.eventHandler = eventHandler;
        this.botStateCash = botStateCash;
        this.eventCash = eventCash;
    }

    public BotApiMethod<!--?--> handle(Message message, BotState botState) {
        long userId = message.getFrom().getId();
        long chatId = message.getChatId();
        SendMessage sendMessage = new SendMessage();
        sendMessage.setChatId(String.valueOf(chatId));
        //if new user
        if (!userDAO.isExist(userId)) {
            return eventHandler.saveNewUser(message, userId, sendMessage);
        }
        //save state in to cache
        botStateCash.saveBotState(userId, botState);
        //if state =...
        switch (botState.name()) {
            case ("START"):
                return menuService.getMainMenuMessage(message.getChatId(),
                        "Воспользуйтесь главным меню", userId);
            case ("ENTERTIME"):
                //set time zone user. for correct sent event
                return eventHandler.enterLocalTimeUser(message);
            case ("MYEVENTS"):
                //list events of user
                return eventHandler.myEventHandler(userId);
            case ("ENTERNUMBEREVENT"):
                //remove event
                return eventHandler.removeEventHandler(message, userId);
            case ("ENTERDESCRIPTION"):
                //enter description for create event
                return eventHandler.enterDescriptionHandler(message, userId);
            case ("ENTERDATE"):
                //enter date for create event
                return eventHandler.enterDateHandler(message, userId);
            case ("CREATE"):
                //start create event, set state to next step
                botStateCash.saveBotState(userId, BotState.ENTERDESCRIPTION);
                //set new event to cache
                eventCash.saveEventCash(userId, new Event());
                sendMessage.setText("Введите описание события");
                return sendMessage;
            case ("ENTERNUMBERFOREDIT"):
                //show to user selected event
                return eventHandler.editHandler(message, userId);
            case ("EDITDESCRIPTION"):
                //save new description in database
                return eventHandler.editDescription(message);
            case ("EDITDATE"):
                //save new date in database
                return eventHandler.editDate(message);
            case ("ALLEVENTS"):
                //only admin
                return eventHandler.allEvents(userId);
            case ("ALLUSERS"):
                //only admin
                return eventHandler.allUsers(userId);
            case ("ONEVENT"):
                // on/off notification
                return eventHandler.onEvent(message);
            case ("ENTERNUMBERUSER"):
                //only admin
                return eventHandler.removeUserHandler(message, userId);
            default:
                throw new IllegalStateException("Unexpected value: " + botState);
        }
    }
}
in the handle method, we check the status of the message we received and send it to the event handler - the EventHandler class. Here we have two new classes, MenuService and EventCash . MenuService – here we create all our menus. EventCash - similar to BotStateCash, it will save parts of our event after input and when the input is completed, we will save the event in the database.
@Service
@Setter
@Getter
// used to save entered event data per session
public class EventCash {

    private final Map<long, event=""> eventMap = new HashMap<>();

    public void saveEventCash(long userId, Event event) {
        eventMap.put(userId, event);
    }
}
</long,>
Well, that is. when we create an event, a new Event object is created in the cache -eventCash.saveEventCash(userId, new Event()); Then we enter a description of the event and add it to the cache:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Then enter the number:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
The CallbackQueryHandler class is similar to MessageHandler , only we process callbackquery messages there. It makes no sense to completely analyze the logic of working with events - EventHandler , there are already too many letters, it is clear from the names of the methods and comments in the code. And I don’t see the point of laying it out completely in text, there are more than 300 lines. Here is a link to the class on Github . The same goes for the MenuService class , where we create our menus. You can read about them in detail on the website of the telegram library manufacturer - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Or in the Telegram reference book - https://tlgrm.ru/docs/bots /api Now we are left with the most delicious part. This is the class for handling EventService messages :
@EnableScheduling
@Service
public class EventService {
    private final EventDAO eventDAO;
    private final EventCashDAO eventCashDAO;

    @Autowired
    public EventService(EventDAO eventDAO, EventCashDAO eventCashDAO) {
        this.eventDAO = eventDAO;
        this.eventCashDAO = eventCashDAO;
    }

    //start service in 0:00 every day
    @Scheduled(cron = "0 0 0 * * *")
    // @Scheduled(fixedRateString = "${eventservice.period}")
    private void eventService() {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int month = calendar.get(Calendar.MONTH);
        int year = calendar.get(Calendar.YEAR);

        //get event list is now date
        List<event> list = eventDAO.findAllEvent().stream().filter(event -> {
            if (event.getUser().isOn()) {
                EventFreq eventFreq = event.getFreq();

                //set user event time
                Calendar calendarUserTime = getDateUserTimeZone(event);

                int day1 = calendarUserTime.get(Calendar.DAY_OF_MONTH);
                int month1 = calendarUserTime.get(Calendar.MONTH);
                int year1 = calendarUserTime.get(Calendar.YEAR);
                switch (eventFreq.name()) {
                    case "TIME": //if one time - remove event
                        if (day == day1 && month == month1 && year == year1) {
                            eventDAO.remove(event);
                            return true;
                        }
                    case "EVERYDAY":
                        return true;
                    case "MONTH":
                        if (day == day1) return true;
                    case "YEAR":
                        if (day == day1 && month == month1) return true;
                    default: return false;
                }
            } else return false;
        }).collect(Collectors.toList());

        for (Event event : list) {
            //set user event time
            Calendar calendarUserTime = getDateUserTimeZone(event);
            int hour1 = calendarUserTime.get(Calendar.HOUR_OF_DAY);
            calendarUserTime.set(year, month, day, hour1, 0, 0);

            String description = event.getDescription();
            String userId = String.valueOf(event.getUser().getId());

            //save the event to the database in case the server reboots.
            EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
            eventCashDAO.save(eventCashEntity);

            //create a thread for the upcoming event with the launch at a specific time
            SendEvent sendEvent = new SendEvent();
            sendEvent.setSendMessage(new SendMessage(userId, description));
            sendEvent.setEventCashId(eventCashEntity.getId());

            new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime());
        }
    }

    private Calendar getDateUserTimeZone(Event event) {
        Calendar calendarUserTime = Calendar.getInstance();
        calendarUserTime.setTime(event.getDate());
        int timeZone = event.getUser().getTimeZone();

        //set correct event time with user timezone
        calendarUserTime.add(Calendar.HOUR_OF_DAY, -timeZone);
        return calendarUserTime;
    }
}

</event>
@EnableScheduling – enable scheduled work in Spring. @Scheduled(cron = "0 0 0 * * *") – we configure the method to run at 0:00 every day calendar.setTime(new Date()); - set server time. We get a list of reminders for today, through the magic of streams and lambda. We go through the received list, set the correct sending time calendarUserTime and... This is where I decided to dodge and launch the processes delayed in time. The Time class in java is responsible for this . new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); For it we need to create a thread:
public class SendEvent extends Thread {


    private long eventCashId;
    private SendMessage sendMessage;

    public SendEvent() {
    }

    @SneakyThrows
    @Override
    public void run() {
        TelegramBot telegramBot = ApplicationContextProvider.getApplicationContext().getBean(TelegramBot.class);
        EventCashDAO eventCashDAO = ApplicationContextProvider.getApplicationContext().getBean(EventCashDAO.class);
        telegramBot.execute(sendMessage);
        //if event it worked, need to remove it from the database of unresolved events
        eventCashDAO.delete(eventCashId);
    }
}
and implementation of TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

    public SimpleTask(SendEvent sendEvent) {
        this.sendEvent = sendEvent;
    }

    @Override
    public void run() {
        sendEvent.start();
    }
}
Yes, I understand perfectly well that you can go through the database every 20 minutes and send messages, but I wrote everything about this at the very beginning)) Here we also encounter the misery of Heroku No. 1. On the free plan, you are given some 550 dino, which is something like the hours of your application’s operation per month. This is not enough for a full month of operation of the application, but if you link a card, you are given another 450 dino, which is enough for your eyes. If you are worried about the card, you can link an empty one, but make sure it contains $0.6... This is a verification amount, it just needs to be in the account. There are no hidden charges unless you change the tariff yourself. On the free plan, there is one more small problem, let's call it No. 1a.. They constantly reboot the servers, or simply send a command to restart the application, in general it reboots every day somewhere at midnight Moscow time, and sometimes at other times. From this, all our processes in memory are deleted. To solve this problem, I came up with the EventCash table. Before sending, events are saved in a separate table:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
And after sending, the following are deleted:
@Override
public void run() {
    TelegramBot telegramBot = ApplicationContextProvider.getApplicationContext().getBean(TelegramBot.class);
    EventCashDAO eventCashDAO = ApplicationContextProvider.getApplicationContext().getBean(EventCashDAO.class);
    telegramBot.execute(sendMessage);
    //if event it worked, need to remove it from the database of unresolved events
    eventCashDAO.delete(eventCashId);
}
ApplicationContextProvider is a special class for getting context on the fly:
@Component
//wrapper to receive Beans
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext ac)
            throws BeansException {
        context = ac;
    }
}
To check for unprocessed events, I made a special service that has a method marked @PostConstruct - it runs after each application start. It picks up all unprocessed events from the database and returns them to memory. Here's a nasty Heroku for you!
@Component
public class SendEventFromCache {

    private final EventCashDAO eventCashDAO;
    private final TelegramBot telegramBot;

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

    @Autowired
    public SendEventFromCache(EventCashDAO eventCashDAO, TelegramBot telegramBot) {
        this.eventCashDAO = eventCashDAO;
        this.telegramBot = telegramBot;
    }

    @PostConstruct
    @SneakyThrows
    //after every restart app  - check unspent events
    private void afterStart() {
        List<eventcashentity> list = eventCashDAO.findAllEventCash();

        SendMessage sendMessage = new SendMessage();
        sendMessage.setChatId(String.valueOf(admin_id));
        sendMessage.setText("Произошла перезагрузка!");
        telegramBot.execute(sendMessage);

        if (!list.isEmpty()) {
            for (EventCashEntity eventCashEntity : list) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(eventCashEntity.getDate());
                SendEvent sendEvent = new SendEvent();
                sendEvent.setSendMessage(new SendMessage(String.valueOf(eventCashEntity.getUserId()), eventCashEntity.getDescription()));
                sendEvent.setEventCashId(eventCashEntity.getId());
                new Timer().schedule(new SimpleTask(sendEvent), calendar.getTime());
            }
        }
    }
}
</eventcashentity>
Our application is ready, and it's time for us to get the Heroku address for the application and database. Your application must be published on Github!!! Go to Heroku.com Click Create New App , enter your application name, select Europe, create app . That's it, the place for the application is ready. If you click open App, the browser will redirect you to the address of your application, this is your webhook address - https://your_name.herokuapp.com/ Register it in telegram, and in the application.property s settings change telegrambot.webHookPath=https: //telegrambotsimpl.herokuapp.com/ to your server.port=5000 can be deleted or commented out. Now let's connect the database. Go to the Resources tab on Heroku, click: Telegram bot - reminder via webHook in Java or say no to Google calendar!  Part 2: - 1 Find Heroku Postgres there , click install : You will be redirected to your database account page. Find it there in Settings/ Telegram bot - reminder via webHook in Java or say no to Google calendar!  Part 2: - 2 There will be all the necessary data from your database. In application.properties everything should now be like this:
#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/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
spring.datasource.username=ulmbeymwyvsxa
spring.datasource.password=4c7646c69dbgeacbk98fa96e8daa6d9b1bl4894e67f3f3ddd6a27fe7b0537fd
Replace the data from your account with yours: In the field jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl .NonValidatingFactory needs to be replaced in bold with the corresponding data from the account (Host, Database). The username, password fields are not difficult to guess. Now we need to create tables in the database, I did this from IDEA. Our script will be useful for creating a database. We add the database as written above: We take Telegram bot - reminder via webHook in Java or say no to Google calendar!  Part 2: - 3 the Host, User, Password, Database field from the account. The URl field is our spring.datasource.url field up to the question mark. We go to the Advanced tab , it should be like this: Telegram bot - reminder via webHook in Java or say no to Google calendar!  Part 2: - 4 If you did everything correctly, then after clicking on test, there will be a green checkmark. Click OK. Right-click on our database and select Jump to query console . Copy our script there and click on execute . The database should be created. 10,000 lines are available to you for free! Everything is ready for Deploy. Go to our application on Heroku in the Deploy section. Select the Github section there: Telegram bot - reminder via webHook in Java or say no to Google calendar!  Part 2: - 5 Link your repository to Heroku. Now your branches will be visible. Don't forget to push your latest changes to .properties. Below, select the branch that will be downloaded and click Deploy branch . If everything is done correctly, you will be notified that the application has been successfully deployed. Don't forget to enable Automatic deploys from .. So that your application starts automatically. By the way, when you push changes to GitHub, Heroku will automatically restart the application. Be careful about this, create a separate thread for bullying, and use the main one only for the working application. Now Cheapness #2! This is the well-known disadvantage of the free plan for Heroku. If there are no incoming messages, the application goes into standby mode, and after receiving a message it will take quite a long time to start, which is not pleasant. There is a simple solution for this - https://uptimerobot.com/ And no, Google ping gadgets won’t help, I don’t even know where this info came from, I Googled this question, and for about 10 years now this topic hasn’t worked for sure, if it worked at all. This application will send HEAD requests to the address you specify for the time you set and, if it does not respond, send a message by email. It won't be difficult for you to figure it out, there aren't enough buttons to get confused)) Congratulations!! If I haven’t forgotten anything and you were attentive, then you have your own application that works for free and never crashes. The opportunity for bullying and experimentation opens up before you. In any case, I am ready to answer questions and accept any criticism! Code: https://github.com/papoff8295/webHookBotForHabr Materials used: https://tlgrm.ru/docs/bots/api - about bots. https://en.wikibooks.org/wiki/Java_Persistence - about relationships in databases. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - Time class and TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – how to post code on Github https://github.com/rubenlagus/TelegramBots - telegram library and a lot of useful information about it.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION