JavaRush /Blog Java /Random-ES /Bot de Telegram: ¡recordatorio a través de webHook en Jav...
Vladimir Popov
Nivel 41

Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google! Parte 2

Publicado en el grupo Random-ES
La segunda parte del proyecto: aquí hay un enlace a la primera: Y así, la clase BotState : para que nuestro bot comprenda lo que se espera de él en un momento determinado, por ejemplo, eliminar un recordatorio, debemos De alguna manera, informe a nuestro robot que los números ingresados ​​y enviados ahora deben tratarse como una ID de recordatorio de la lista y deben eliminarse. Por lo tanto, después de hacer clic en el botón "Eliminar" , el bot pasa al estado BotState.ENTERNUMBEREVENT , esta es una clase Enum especialmente creada con estados 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
}
Y ahora se espera que ingresemos números: " Ingrese el número de recordatorio de la lista ". Después de ingresar, irán al método deseado para su procesamiento. Aquí está nuestro cambio de estado:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
Un mapa normal con una identificación de usuario y su estado. El campo int adminId es para mí) A continuación, la lógica del método handleUpdate comprobará qué tipo de mensaje es este. ¿ Consulta de devolución de llamada o simplemente mensaje de texto? Si se trata de texto normal, vamos al método handleInputMessage , donde procesamos los botones del menú principal, y si se hizo clic en ellos, establecemos el estado deseado, pero si no se hizo clic en ellos y es texto desconocido, entonces configuramos el estado desde el caché, si no está allí, configuramos el estado inicial. Luego, el texto pasa a procesar el método de manejo con el estado que necesitamos. Ahora presentamos la lógica de la clase MessageHandler , que se encarga de procesar los mensajes dependiendo del estado del 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);
        }
    }
}
en el método handle, verificamos el estado del mensaje que recibimos y lo enviamos al controlador de eventos: la clase EventHandler. Aquí tenemos dos clases nuevas, MenuService y EventCash . MenuService – aquí creamos todos nuestros menús. EventCash : similar a BotStateCash, guardará partes de nuestro evento después de la entrada y cuando se complete la entrada, guardaremos el evento en la base de datos.
@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,>
Bueno, eso es. cuando creamos un evento, se crea un nuevo objeto Evento en el caché -eventCash.saveEventCash(userId, new Event()); Luego ingresamos una descripción del evento y la agregamos al caché:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Luego ingrese el número:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
La clase CallbackQueryHandler es similar a MessageHandler , solo que procesamos mensajes de devolución de llamada allí. No tiene sentido analizar completamente la lógica de trabajar con eventos: EventHandler , ya hay demasiadas letras, se desprende de los nombres de los métodos y comentarios en el código. Y no veo el sentido de exponerlo completamente en texto, hay más de 300 líneas. Aquí hay un enlace a la clase en Github . Lo mismo ocurre con la clase MenuService , donde creamos nuestros menús. Puede leer sobre ellos en detalle en el sitio web del fabricante de la biblioteca de Telegram: https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md o en el libro de referencia de Telegram: https:// tlgrm.ru/docs/bots /api Ahora nos queda la parte más deliciosa. Esta es la clase para manejar mensajes de 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 : habilita el trabajo programado en Spring. @Scheduled(cron = "0 0 0 * * *") – configuramos el método para que se ejecute a las 0:00 todos los días calendar.setTime(new Date()); - establecer la hora del servidor. Obtenemos una lista de recordatorios para hoy, a través de la magia de streams y lambda. Revisamos la lista de recibidos, configuramos el tiempo de envío correcto calendarUserTime y... Aquí es donde decidí esquivar y lanzar los procesos retrasados ​​​​en el tiempo. La clase Time en Java es responsable de esto . new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); Para ello necesitamos crear un hilo:
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);
    }
}
e implementación de TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Sí, entiendo perfectamente que puedes revisar la base de datos cada 20 minutos y enviar mensajes, pero escribí todo sobre esto desde el principio)) Aquí también nos encontramos con la miseria de Heroku No. 1. En el plan gratuito, recibes unos 550 dinosaurios, que es algo así como las horas de funcionamiento de tu aplicación por mes. Esto no es suficiente para un mes completo de funcionamiento de la aplicación, pero si vinculas una tarjeta, recibirás otros 450 dinosaurios, que son suficientes para tus ojos. Si le preocupa la tarjeta, puede vincular una vacía, pero asegúrese de que contenga $0,6... Este es un monto de verificación, solo necesita estar en la cuenta. No hay cargos ocultos a menos que usted mismo cambie la tarifa. En el plan gratuito, hay un pequeño problema más, llamémoslo No. 1a... Reinician constantemente los servidores, o simplemente envían un comando para reiniciar la aplicación, en general se reinicia todos los días en algún lugar a la medianoche, hora de Moscú, y a veces en otros tiempos. A partir de esto, se borran todos nuestros procesos en la memoria. Para resolver este problema, se me ocurrió la tabla EventCash. Antes de enviar, los eventos se guardan en una tabla separada:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
Y después del envío se elimina lo siguiente:
@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 es una clase especial para obtener contexto sobre la marcha:
@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;
    }
}
Para comprobar si hay eventos no procesados, creé un servicio especial que tiene un método marcado como @PostConstruct : se ejecuta después de cada inicio de la aplicación. Recoge todos los eventos no procesados ​​de la base de datos y los devuelve a la memoria. ¡Aquí tienes un Heroku desagradable!
@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>
Nuestra aplicación está lista y es hora de que obtengamos la dirección de Heroku para la aplicación y la base de datos. ¡¡¡Tu aplicación debe estar publicada en Github!!! Vaya a Heroku.com. Haga clic en Crear nueva aplicación , ingrese el nombre de su aplicación, seleccione Europa, cree aplicación . Todo, el lugar para la aplicación está listo. Si hace clic en Abrir aplicación, el navegador lo redirigirá a la dirección de su aplicación, esta es su dirección de webhook: https://your_name.herokuapp.com/ Regístrela en Telegram y en la configuración de application.property cambie Telegrambot. webHookPath=https: //telegrambotsimpl.herokuapp.com/ a su server.port=5000 se puede eliminar o comentar. Ahora conectemos la base de datos. Vaya a la pestaña Recursos en Heroku, haga clic en: Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google!  Parte 2: - 1 Encuentre Heroku Postgres allí , haga clic en instalar : será redirigido a la página de su cuenta de base de datos. Encuéntrelo allí en Configuración/ Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google!  Parte 2: - 2 Allí estarán todos los datos necesarios de su base de datos. En application.properties todo debería ser ahora así:
#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
Reemplaza los datos de tu cuenta por los tuyos: En el campo jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org. postgresql.ssl .NonValidatingFactory debe reemplazarse en negrita con los datos correspondientes de la cuenta (Host, Base de datos). Los campos de nombre de usuario y contraseña no son difíciles de adivinar. Ahora necesitamos crear tablas en la base de datos, lo hice desde IDEA. Nuestro script será útil para crear una base de datos. Agregamos la base de datos como está escrito arriba: tomamos Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google!  Parte 2: - 3 el campo Host, Usuario, Contraseña, Base de datos de la cuenta. El campo URL es nuestro campo spring.datasource.url hasta el signo de interrogación. Vamos a la pestaña Avanzado , debería ser así: Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google!  Parte 2: - 4 Si hiciste todo correctamente, luego de hacer clic en prueba, habrá una marca de verificación verde. Haga clic en Aceptar. Haga clic derecho en nuestra base de datos y seleccione Saltar a la consola de consultas . Copie nuestro script allí y haga clic en ejecutar . Se debe crear la base de datos. ¡10.000 líneas están disponibles para usted de forma gratuita! Todo está listo para la implementación. Vaya a nuestra aplicación en Heroku en la sección Implementar. Selecciona la sección Github allí: Bot de Telegram: ¡recordatorio a través de webHook en Java o di no al calendario de Google!  Parte 2: - 5 Vincula tu repositorio a Heroku. Ahora tus ramas serán visibles. No olvide enviar sus últimos cambios a .properties. A continuación, seleccione la rama que se descargará y haga clic en Implementar rama . Si todo se hace correctamente, se le notificará que la aplicación se ha implementado correctamente. No olvide habilitar Implementaciones automáticas desde ... Para que su aplicación se inicie automáticamente. Por cierto, cuando envías cambios a GitHub, Heroku reiniciará automáticamente la aplicación. Tenga cuidado con esto, cree un hilo separado para el acoso y use el principal solo para la aplicación que funciona. ¡Ahora Baratura #2! Ésta es la conocida desventaja del plan gratuito de Heroku. Si no hay mensajes entrantes, la aplicación entra en modo de espera y, después de recibir un mensaje, tardará bastante en iniciarse, lo que no es agradable. Existe una solución sencilla para esto: https://uptimerobot.com/ Y no, los dispositivos de ping de Google no ayudarán, ni siquiera sé de dónde vino esta información, busqué en Google esta pregunta y durante aproximadamente 10 años este tema no ha funcionado con seguridad, si es que funcionó en absoluto. Esta aplicación enviará solicitudes HEAD a la dirección que especifiques durante el tiempo que establezcas y, si no responde, enviará un mensaje por correo electrónico. No te resultará difícil entenderlo, no hay suficientes botones como para confundirte)) ¡¡Enhorabuena!! Si no se me ha olvidado nada y estuviste atento, entonces tienes tu propia aplicación que funciona gratis y nunca falla. Ante ti se abre la oportunidad de intimidar y experimentar. En cualquier caso, ¡estoy dispuesto a responder preguntas y aceptar cualquier crítica! Código: https://github.com/papoff8295/webHookBotForHabr Materiales utilizados: https://tlgrm.ru/docs/bots/api - sobre bots. https://en.wikibooks.org/wiki/Java_Persistence: sobre relaciones en bases de datos. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - Clase de tiempo y TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – cómo publicar código en Github https://github.com/rubenlagus/TelegramBots - biblioteca de telegramas y mucha información útil al respecto.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION