JavaRush /Java 博客 /Random-ZH /Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不!第2部分...
Vladimir Popov
第 41 级

Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不!第2部分

已在 Random-ZH 群组中发布
该项目的第二部分 - 这是第一部分的链接:因此BotState类:为了让我们的机器人了解在某个时间点对其的期望,例如删除提醒,我们需要以某种方式让我们的机器人知道现在输入和发送的号码应该被视为列表中的提醒 ID,并且应该被删除。因此,点击“删除”按钮后,机器人会进入BotState.ENTERNUMBEREVENT状态,这是一个专门创建的具有机器人状态的 Enum 类。
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
}
现在我们需要输入数字 - “从列表中输入提醒号码。” 输入后,他们将转到所需的处理方法。这是我们的状态开关:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
带有用户 ID 及其状态的常规地图。int adminId字段是给我的)接下来, handleUpdate方法的逻辑将检查这是什么类型的消息?回调查询还是只是文本?如果这是常规文本,那么我们转到handleInputMessage方法,在该方法中我们处理主菜单按钮,如果单击了它们,那么我们设置所需的状态,但如果没有单击它们并且这是不熟悉的文本,那么我们从缓存中设置状态,如果不存在,则设置起始状态。然后正文进入处理具有我们需要的状态的句柄方法。现在我们介绍MessageHandler类的逻辑,它负责根据机器人的状态处理消息:
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);
        }
    }
}
在handle方法中,我们检查收到的消息的状态并将其发送到事件处理程序——EventHandler类。 这里我们有两个新类:MenuService 和 EventCashMenuService – 在这里我们创建所有菜单。 EventCash - 与 BotStateCash 类似它会在输入后保存我们的部分事件,当输入完成后,我们会将事件保存在数据库中。
@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,>
嗯,就是这样。当我们创建一个事件时,会在缓存中创建一个新的Event对象 -eventCash.saveEventCash(userId, new Event()); 然后我们输入事件的描述并将其添加到缓存中:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
然后输入号码:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
CallbackQueryHandler 类与MessageHandler 类似,只是我们在那里处理回调查询消息。完全分析事件处理逻辑 - EventHandler是没有意义的,字母已经太多了,从代码中的方法名称和注释中可以清楚地看出。而且我看不出将其完全用文本来表达的意义,有 300 多行。这是Github上该课程的链接。对于我们创建菜单的MenuService类也是如此。您可以在 Telegram 库制造商的网站上详细了解它们 - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md 或者在 Telegram 参考书中 - https:// tlgrm.ru/docs/bots /api 现在我们剩下最美味的部分了。这是用于处理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 – 在 Spring 中启用计划工作。 @Scheduled(cron = "0 0 0 * * *") – 我们将该方法配置为每天 0:00 运行 calendar.setTime(new Date()); - 设置服务器时间。通过流和 lambda 的魔力,我们获得了今天的提醒列表。我们检查收到的列表,设置正确的发送时间calendarUserTime,然后...这是我决定躲避并及时启动延迟进程的地方。java 中的Time类负责此工作。 new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); 为此我们需要创建一个线程:
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);
    }
}
以及TimerTask 的实现
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
是的,我完全理解你可以每 20 分钟检查一次数据库并发送消息,但我在一开始就写了有关此的所有内容))在这里我们也遇到了 Heroku No.1 的痛苦。在免费计划中,您将获得大约 550 个恐龙,这相当于您的应用程序每月运行的时间。这对于应用程序运行一整月来说还不够,但是如果你链接一张卡,你会得到另外 450 恐龙,这足够你的眼睛了。如果您担心该卡,您可以链接一张空卡,但请确保其中包含 0.6 美元...这是验证金额,只需在帐户中即可。除非您自己更改费率,否则没有隐藏费用。在免费计划中,还有一个小问题,让我们称之为No.1a..他们不断地重新启动服务器,或者只是发送一个命令来重新启动应用程序,一般来说,它每天都会在莫斯科时间午夜的某个地方重新启动,有时会重新启动。在其他时间。由此,我们内存中的所有进程都被删除。为了解决这个问题,我想出了EventCash表。在发送之前,事件被保存在一个单独的表中:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
发送后,以下内容将被删除:
@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是一个用于动态获取上下文的特殊类:
@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;
    }
}
为了检查未处理的事件,我创建了一个特殊的服务,其中有一个标记为@PostConstruct 的方法- 它在每个应用程序启动后运行。它从数据库中获取所有未处理的事件并将它们返回到内存中。这是给你的一个讨厌的 Heroku!
@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>
我们的应用程序已准备就绪,现在是时候获取应用程序和数据库的 Heroku 地址了。你的应用程序必须发布在Github上!!!前往 Heroku.com 单击Create New App,输入您的应用程序名称,选择Europe,创建应用程序。就这样,应用程序的地方就准备好了。如果您点击打开应用程序,浏览器会将您重定向到您的应用程序的地址,这是您的 webhook 地址 - https://your_name.herokuapp.com/ 在 telegram 中注册它,并在application.property的设置中更改telegrambot。 webHookPath=https://telegrambotsimpl.herokuapp.com/到你的 server.port=5000 可以删除或注释掉。现在让我们连接数据库。转到Heroku 上的 资源Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不! 第 2 部分:- 1选项卡,单击:在那里 查找Heroku Postgres,单击安装:您将被重定向到您的数据库帐户页面。在“设置/”中找到它, Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不! 第 2 部分:- 2 其中将包含数据库中的所有必要数据。在application.properties中,一切现在应该是这样的:
#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
将您帐户中的数据替换为您的帐户中的数据:在字段 jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org 中。 postgresql.ssl .NonValidatingFactory 需要用粗体字替换为账户(Host、Database)对应的数据,其中的用户名、密码字段不难猜测。现在我们需要在数据库中创建表,我是在 IDEA 中完成的。我们的脚本对于创建数据库非常有用。我们按照上面的方式添加数据库: 我们从帐户中获取Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不! 第 2 部分:- 3 主机、用户、密码、数据库字段。URl字段是我们的 spring.datasource.url 字段,直到问号。我们进入“高级”选项卡,它应该是这样的: Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不! 第 2 部分:- 4 如果您所做的一切正确,那么单击“测试”后,将会有一个绿色的复选标记。单击“确定”。右键单击我们的数据库并选择跳转到查询控制台。将我们的脚本复制到那里并单击执行。应创建数据库。10,000条线路免费供您使用!一切准备就绪,可以部署了。转到 Heroku 上的“部署”部分中的应用程序。选择此处的 Github 部分: Telegram 机器人 - 通过 Java 中的 webHook 进行提醒或对 Google 日历说不! 第 2 部分:- 5 将您的存储库链接到 Heroku。现在你的分支将可见。不要忘记将最新更改推送到 .properties。在下面,选择要下载的分支,然后单击部署分支。如果一切都正确完成,您将收到应用程序已成功部署的通知。不要忘记从 .. 启用自动部署,以便您的应用程序自动启动。顺便说一句,当您将更改推送到 GitHub 时,Heroku 会自动重新启动应用程序。请小心这一点,创建一个单独的线程用于欺凌,并仅将主线程用于工作应用程序。现在便宜#2!这是 Heroku 免费计划众所周知的缺点。如果没有传入消息,应用程序将进入待机模式,并且在收到消息后将需要相当长的时间才能启动,这并不令人愉快。有一个简单的解决方案 - https://uptimerobot.com/ 不,Google ping 小工具不会有帮助,我什至不知道这些信息来自哪里,我用 Google 搜索了这个问题,大约 10 年来,这个话题一直没有确定的作用,如果它真的有效的话。此应用程序将在您设置的时间内向您指定的地址发送 HEAD 请求,如果没有响应,则通过电子邮件发送消息。对你来说,弄清楚它并不困难,没有足够的按钮让你感到困惑))恭喜!如果我没有忘记任何事情并且您很专心,那么您就拥有了自己的应用程序,可以免费运行并且永远不会崩溃。欺凌和实验的机会就摆在你面前。无论如何,我愿意回答问题并接受任何批评!代码:https://github.com/papoff8295/webHookBotForHabr 使用的材料:https://tlgrm.ru/docs/bots/api - 关于机器人。https://en.wikibooks.org/wiki/Java_Persistence - 关于数据库中的关系。https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - 时间类和 TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – 如何在 Github 上发布代码 https://github.com/rubenlagus/TelegramBots – 电报库以及许多有关它的有用信息。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION