JavaRush /Blog Java /Random-FR /Bot Telegram - rappel via webHook en Java ou dites non au...
Vladimir Popov
Niveau 41

Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google ! Partie 2

Publié dans le groupe Random-FR
La deuxième partie du projet - voici un lien vers la première : Et donc la classe BotState : Pour que notre bot comprenne ce qu'on attend de lui à un moment donné, par exemple supprimer un rappel, nous devons D'une manière ou d'une autre, faites savoir à notre bot que les numéros sont saisis et envoyés maintenant doivent être traités comme un identifiant de rappel de la liste et doivent être supprimés. Par conséquent, après avoir cliqué sur le bouton « Supprimer » , le bot passe à l' état BotState.ENTERNUMBEREVENT , il s'agit d'une classe Enum spécialement créée avec des états de bot.
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
}
Et maintenant, nous sommes censés saisir des numéros - “ Entrez le numéro de rappel dans la liste .” Après avoir saisi quoi, ils passeront à la méthode de traitement souhaitée. Voici notre changement d'état :
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
Une carte standard avec un identifiant utilisateur et son statut. Le champ int adminId est pour moi) Ensuite, la logique de la méthode handleUpdate vérifiera de quel type de message il s'agit ? Requête de rappel ou simplement SMS ? S'il s'agit d'un texte normal, nous passons à la méthode handleInputMessage , où nous traitons les boutons du menu principal, et s'ils ont été cliqués, nous définissons l'état souhaité, mais s'ils n'ont pas été cliqués et qu'il s'agit d'un texte inconnu, alors nous définissons l'état à partir du cache, s'il n'y est pas, alors nous définissons l'état de départ. Ensuite, le texte passe au traitement de la méthode handle avec l’état dont nous avons besoin. Nous présentons maintenant la logique de la classe MessageHandler , qui est chargée de traiter les messages en fonction de l'état du 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);
        }
    }
}
dans la méthode handle, nous vérifions l'état du message que nous avons reçu et l'envoyons au gestionnaire d'événements - la classe EventHandler. Nous avons ici deux nouvelles classes, MenuService et EventCash . MenuService – ici nous créons tous nos menus. EventCash - similaire à BotStateCash, il enregistrera des parties de notre événement après la saisie et lorsque la saisie sera terminée, nous enregistrerons l'événement dans la base de données.
@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,>
Eh bien, c'est vrai. lorsque nous créons un événement, un nouvel objet Event est créé dans le cache -eventCash.saveEventCash(userId, new Event()); Ensuite, nous entrons une description de l'événement et l'ajoutons au cache :
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Entrez ensuite le numéro :
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
La classe CallbackQueryHandler est similaire à MessageHandler , sauf que nous y traitons les messages de requête de rappel. Cela n'a aucun sens d'analyser complètement la logique du travail avec les événements - EventHandler , il y a déjà trop de lettres, cela ressort clairement des noms des méthodes et des commentaires dans le code. Et je ne vois pas l’intérêt de le mettre entièrement en texte, il y a plus de 300 lignes. Voici un lien vers le cours sur Github . Il en va de même pour la classe MenuService , où nous créons nos menus. Vous pouvez les lire en détail sur le site Web du fabricant de la bibliothèque Telegram - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Ou dans le livre de référence Telegram - https:// tlgrm.ru/docs/bots /api Il nous reste maintenant la partie la plus délicieuse. Il s'agit de la classe permettant de gérer les messages EventService :
@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 – activez le travail planifié au printemps. @Scheduled(cron = "0 0 0 * * *") – nous configurons la méthode pour qu'elle s'exécute à 0h00 tous les jours calendrier.setTime(new Date()); - définir l'heure du serveur. Nous obtenons une liste de rappels pour aujourd'hui, grâce à la magie des streams et du lambda. Nous parcourons la liste reçue, définissons l'heure d'envoi correcte calendrierUserTime et... C'est là que j'ai décidé d'esquiver et de lancer les processus retardés dans le temps. La classe Time en Java en est responsable . new Timer().schedule(new SimpleTask(sendEvent), calendrierUserTime.getTime()); Pour cela, nous devons créer un fil de discussion :
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);
    }
}
et implémentation de TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Oui, je comprends parfaitement que vous pouvez parcourir la base de données toutes les 20 minutes et envoyer des messages, mais j'ai tout écrit à ce sujet au tout début)) Ici, nous rencontrons également la misère d'Heroku n°1. Avec le forfait gratuit, vous recevez environ 550 dinos, ce qui équivaut à peu près aux heures de fonctionnement de votre application par mois. Ce n'est pas suffisant pour un mois complet de fonctionnement de l'application, mais si vous associez une carte, vous recevez 450 dino supplémentaires, ce qui est suffisant pour vos yeux. Si la carte vous inquiète, vous pouvez en associer une vide, mais assurez-vous qu'elle contient 0,6 $... Il s'agit d'un montant de vérification, il doit simplement être sur le compte. Il n'y a pas de frais cachés, sauf si vous modifiez vous-même le tarif. Sur le plan gratuit, il y a encore un petit problème, appelons-le n°1a.. Ils redémarrent constamment les serveurs, ou envoient simplement une commande pour redémarrer l'application, en général elle redémarre tous les jours quelque part à minuit, heure de Moscou, et parfois d'autres fois. A partir de là, tous nos processus en mémoire sont supprimés. Pour résoudre ce problème, j'ai créé la table EventCash. Avant l'envoi, les événements sont enregistrés dans un tableau séparé :
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
Et après envoi, sont supprimés :
@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 est une classe spéciale pour obtenir du contexte à la volée :
@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;
    }
}
Pour vérifier les événements non traités, j'ai créé un service spécial doté d'une méthode marquée @PostConstruct - il s'exécute après chaque démarrage d'application. Il récupère tous les événements non traités de la base de données et les renvoie en mémoire. Voici un méchant Heroku pour vous !
@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>
Notre application est prête et il est temps pour nous d'obtenir l'adresse Heroku de l'application et de la base de données. Votre candidature doit être publiée sur Github !!! Accédez à Heroku.com Cliquez sur Créer une nouvelle application , entrez le nom de votre application, sélectionnez Europe, créez une application . Ça y est, le lieu de candidature est prêt. Si vous cliquez sur Ouvrir l'application, le navigateur vous redirigera vers l'adresse de votre application, il s'agit de votre adresse de webhook - https://votre_nom.herokuapp.com/ Enregistrez-la dans le télégramme et dans les paramètres de l'application.property , modifiez le télégramme. webHookPath=https : //telegrambotsimpl.herokuapp.com/ sur votre serveur.port=5000 peut être supprimé ou commenté. Connectons maintenant la base de données. Allez dans l' onglet Ressources sur Heroku, cliquez sur : Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google !  Partie 2 : - 1 Recherchez Heroku Postgres ici , cliquez sur Installer : Vous serez redirigé vers la page de votre compte de base de données. Trouvez-le là dans Paramètres/ Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google !  Partie 2 : - 2 Il y aura toutes les données nécessaires de votre base de données. Dans application.properties, tout devrait maintenant ressembler à ceci :
#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
Remplacez les données de votre compte par les vôtres : Dans le champ jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org. postgresql.ssl .NonValidatingFactory doit être remplacé en gras par les données correspondantes du compte (Hôte, Base de données). Les champs nom d'utilisateur et mot de passe ne sont pas difficiles à deviner. Nous devons maintenant créer des tables dans la base de données, je l'ai fait depuis IDEA. Notre script sera utile pour créer une base de données. Nous ajoutons la base de données comme écrit ci-dessus : Nous retirons Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google !  Partie 2 : - 3 le champ Hôte, Utilisateur, Mot de passe, Base de données du compte. Le champ URl est notre champ spring.datasource.url jusqu'au point d'interrogation. Nous allons dans l' onglet Avancé , cela devrait être comme ceci : Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google !  Partie 2 : - 4 Si vous avez tout fait correctement, alors après avoir cliqué sur tester, il y aura une coche verte. Cliquez sur OK. Faites un clic droit sur notre base de données et sélectionnez Aller à la console de requête . Copiez-y notre script et cliquez sur exécuter . La base de données doit être créée. 10 000 lignes s'offrent à vous gratuitement ! Tout est prêt pour le déploiement. Accédez à notre application sur Heroku dans la section Déployer. Sélectionnez ici la section Github : Bot Telegram - rappel via webHook en Java ou dites non au calendrier Google !  Partie 2 : - 5 Liez votre référentiel à Heroku. Vos branches seront désormais visibles. N'oubliez pas de transmettre vos dernières modifications à .properties. Ci-dessous, sélectionnez la branche qui sera téléchargée et cliquez sur Déployer la branche . Si tout est fait correctement, vous serez averti que l'application a été déployée avec succès. N'oubliez pas d'activer les déploiements automatiques à partir de .. Pour que votre application démarre automatiquement. À propos, lorsque vous transmettez des modifications à GitHub, Heroku redémarrera automatiquement l'application. Soyez prudent à ce sujet, créez un fil de discussion séparé pour l'intimidation et utilisez le fil principal uniquement pour l'application qui fonctionne. Maintenant bon marché n°2 ! C’est l’inconvénient bien connu du forfait gratuit pour Heroku. S'il n'y a pas de messages entrants, l'application passe en mode veille, et après avoir reçu un message, elle mettra beaucoup de temps à démarrer, ce qui n'est pas agréable. Il existe une solution simple pour cela - https://uptimerobot.com/ Et non, les gadgets Google ping n'aideront pas, je ne sais même pas d'où viennent ces informations, j'ai recherché cette question sur Google, et depuis environ 10 ans maintenant, ce sujet n'a pas fonctionné, voire pas du tout. Cette application enverra les requêtes HEAD à l'adresse que vous spécifiez pour l'heure que vous avez définie et, si elle ne répond pas, enverra un message par e-mail. Ce ne sera pas difficile pour vous de le comprendre, il n'y a pas assez de boutons pour se confondre)) Félicitations !! Si je n’ai rien oublié et que vous avez été attentif, alors vous disposez de votre propre application qui fonctionne gratuitement et ne plante jamais. L’opportunité d’intimidation et d’expérimentation s’ouvre devant vous. Dans tous les cas, je suis prêt à répondre aux questions et à accepter toute critique ! Code : https://github.com/papoff8295/webHookBotForHabr Matériaux utilisés : https://tlgrm.ru/docs/bots/api - à propos des robots. https://en.wikibooks.org/wiki/Java_Persistence - sur les relations dans les bases de données. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-special-time-in-java - Classe de temps et TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – comment publier du code sur Github https://github.com/rubenlagus/TelegramBots – bibliothèque de télégrammes et de nombreuses informations utiles à ce sujet.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION