JavaRush /Blogue Java /Random-PT /Bot do Telegram - lembrete via webHook em Java ou diga nã...
Vladimir Popov
Nível 41

Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google! Parte 2

Publicado no grupo Random-PT
A segunda parte do projeto - aqui está um link para a primeira: E então a classe BotState : Para que nosso bot entenda o que se espera dele em um determinado momento, por exemplo, excluindo um lembrete, precisamos de alguma forma, informe ao nosso bot que os números inseridos e enviados agora devem ser tratados como um ID de lembrete da lista e devem ser excluídos. Portanto, após clicar no botão “Excluir” , o bot entra no estado BotState.ENTERNUMBEREVENT , esta é uma classe Enum especialmente criada com 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
}
E agora devemos inserir números - “ Digite o número do lembrete na lista ”. Depois de entrar, eles irão para o método de processamento desejado. Aqui está nossa mudança 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,>
Um mapa normal com um ID de usuário e seu status. O campo int adminId é para mim) A seguir, a lógica do método handleUpdate irá verificar que tipo de mensagem é essa? Consulta de retorno de chamada ou apenas texto? Se este for um texto normal, então vamos para o método handleInputMessage , onde processamos os botões do menu principal, e se eles foram clicados, definimos o estado desejado, mas se eles não foram clicados e este é um texto desconhecido, então nós definimos o estado do cache, se não estiver lá, definimos o estado inicial. Em seguida, o texto processa o método handle com o estado que precisamos. Agora apresentamos a lógica da classe MessageHandler , que é responsável por processar mensagens dependendo do estado do 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);
        }
    }
}
no método handle, verificamos o status da mensagem que recebemos e a enviamos para o manipulador de eventos - a classe EventHandler. Aqui temos duas novas classes, MenuService e EventCash . MenuService – aqui criamos todos os nossos menus. EventCash - semelhante ao BotStateCash, ele salvará partes do nosso evento após a entrada e quando a entrada for concluída, salvaremos o evento no banco de dados.
@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,>
Bem, isso é. quando criamos um evento, um novo objeto Event é criado no cache -eventCash.saveEventCash(userId, new Event()); Em seguida, inserimos uma descrição do evento e adicionamos ao cache:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Em seguida, digite o número:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
A classe CallbackQueryHandler é semelhante a MessageHandler , apenas processamos mensagens de callbackquery lá. Não faz sentido analisar completamente a lógica de trabalhar com eventos - EventHandler , já existem muitas letras, fica claro pelos nomes dos métodos e comentários no código. E não vejo sentido em expor tudo completamente em texto, são mais de 300 linhas. Aqui está um link para a aula no Github . O mesmo vale para a classe MenuService , onde criamos nossos menus. Você pode ler sobre eles em detalhes no site do fabricante da biblioteca de telegramas - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Ou no livro de referência do Telegram - https:// tlgrm.ru/docs/bots /api Agora ficamos com a parte mais deliciosa. Esta é a classe para lidar com mensagens 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 o trabalho agendado no Spring. @Scheduled(cron = "0 0 0 * * *") – configuramos o método para rodar às 0:00 todos os dias calendar.setTime(new Date()); - definir a hora do servidor. Recebemos uma lista de lembretes para hoje, através da magia dos streams e do lambda. Percorremos a lista recebida, definimos o horário de envio correto calendarUserTime e... Foi aí que resolvi me esquivar e lançar os processos atrasados. A classe Time em java é responsável por isso . novo Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); Para isso precisamos criar um tópico:
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 implementação do TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Sim, entendo perfeitamente que você pode acessar o banco de dados a cada 20 minutos e enviar mensagens, mas escrevi tudo sobre isso logo no início)) Aqui também encontramos a miséria do Heroku nº 1. No plano gratuito, você recebe cerca de 550 dino, que é algo como as horas de funcionamento do seu aplicativo por mês. Isso não é suficiente para um mês inteiro de funcionamento do aplicativo, mas se você vincular um cartão, ganha mais 450 dino, o que dá para os seus olhos. Se você está preocupado com o cartão, pode vincular um vazio, mas certifique-se de que contém $ 0,6... Este é um valor de verificação, só precisa estar na conta. Não há cobranças ocultas, a menos que você mesmo altere a tarifa. No plano gratuito, há mais um pequeno problema, vamos chamá-lo de nº 1a.. Eles reinicializam constantemente os servidores, ou simplesmente enviam um comando para reiniciar o aplicativo, em geral ele reinicia todos os dias em algum lugar à meia-noite, horário de Moscou, e às vezes em outras ocasiões. A partir disso, todos os nossos processos na memória são deletados. Para resolver esse problema, criei a tabela EventCash. Antes do envio, os eventos são salvos em uma tabela separada:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
E após o envio, são excluídos os seguintes:
@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 é uma classe especial para obter contexto rapidamente:
@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 verificar eventos não processados, criei um serviço especial que possui um método marcado como @PostConstruct - ele é executado após o início de cada aplicativo. Ele coleta todos os eventos não processados ​​do banco de dados e os retorna para a memória. Aqui está um Heroku desagradável para você!
@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>
Nosso aplicativo está pronto e é hora de obtermos o endereço Heroku do aplicativo e do banco de dados. Sua aplicação deve estar publicada no Github!!! Vá para Heroku.com Clique em Criar novo aplicativo , digite o nome do seu aplicativo, selecione Europa, crie aplicativo . Pronto, o local da aplicação está pronto. Se você clicar em abrir aplicativo, o navegador irá redirecioná-lo para o endereço do seu aplicativo, este é o seu endereço de webhook - https://seu_nome.herokuapp.com/ Registre-o no telegrama e nas configurações do application.property altere o telegrambot. webHookPath=https: //telegrambotsimpl.herokuapp.com/ para seu server.port=5000 pode ser excluído ou comentado. Agora vamos conectar o banco de dados. Vá para a guia Recursos no Heroku, clique em: Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google!  Parte 2: - 1 Encontre o Heroku Postgres lá , clique em instalar : você será redirecionado para a página da sua conta de banco de dados. Encontre-o em Configurações/ Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google!  Parte 2: - 2 Haverá todos os dados necessários do seu banco de dados. Em application.properties agora tudo deve ficar assim:
#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
Substitua os dados da sua conta pelos seus: No 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 precisa ser substituído em negrito pelos dados correspondentes da conta (Host, Banco de dados).Os campos de nome de usuário e senha não são difíceis de adivinhar. Agora precisamos criar tabelas no banco de dados, fiz isso no IDEA. Nosso script será útil para criar um banco de dados. Adicionamos o banco de dados conforme escrito acima: Pegamos Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google!  Parte 2: - 3 o campo Host, Usuário, Senha, Banco de dados da conta. O campo URl é nosso campo spring.datasource.url até o ponto de interrogação. Vamos para a aba Avançado , deve ser assim: Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google!  Parte 2: - 4 Se você fez tudo corretamente, depois de clicar em teste, haverá uma marca de seleção verde. Clique OK. Clique com o botão direito em nosso banco de dados e selecione Ir para o console de consulta . Copie nosso script lá e clique em executar . O banco de dados deve ser criado. 10.000 linhas estão disponíveis para você gratuitamente! Tudo está pronto para implantação. Acesse nosso aplicativo no Heroku na seção Deploy. Selecione a seção Github: Bot do Telegram - lembrete via webHook em Java ou diga não ao calendário do Google!  Parte 2: - 5 Vincule seu repositório ao Heroku. Agora seus galhos estarão visíveis. Não se esqueça de enviar suas alterações mais recentes para .properties. Abaixo, selecione o branch que será baixado e clique em Deploy branch . Se tudo for feito corretamente, você será notificado de que o aplicativo foi implantado com sucesso. Não se esqueça de habilitar Implantações automáticas de .. Para que seu aplicativo seja iniciado automaticamente. A propósito, quando você envia alterações para o GitHub, o Heroku reinicia automaticamente o aplicativo. Tenha cuidado com isso, crie um tópico separado para bullying e use o principal apenas para o aplicativo funcional. Agora o preço baixo # 2! Esta é a conhecida desvantagem do plano gratuito do Heroku. Se não houver mensagens recebidas, o aplicativo entra em modo standby e após receber uma mensagem demorará bastante para iniciar, o que não é agradável. Existe uma solução simples para isso - https://uptimerobot.com/ E não, os gadgets de ping do Google não vão ajudar, nem sei de onde veio essa informação, pesquisei essa pergunta no Google e, há cerca de 10 anos, esse tópico não funciona com certeza, se é que funcionou. Este aplicativo enviará solicitações HEAD para o endereço que você especificar no horário definido e, caso não responda, enviará uma mensagem por e-mail. Não será difícil para você descobrir, não há botões suficientes para se confundir)) Parabéns!! Se não esqueci de nada e você ficou atento, então você tem seu próprio aplicativo que funciona de graça e nunca trava. A oportunidade para bullying e experimentação se abre diante de você. De qualquer forma, estou pronto para esclarecer dúvidas e aceitar qualquer crítica! Código: https://github.com/papoff8295/webHookBotForHabr Materiais usados: https://tlgrm.ru/docs/bots/api - sobre bots. https://en.wikibooks.org/wiki/Java_Persistence - sobre relacionamentos em bancos de dados. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - Classe de tempo e TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – como postar código no Github https://github.com/rubenlagus/TelegramBots – biblioteca de telegramas e muitas informações úteis sobre ela.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION