JavaRush /Blog Java /Random-VI /Telegram bot - nhắc nhở qua webHook trong Java hoặc nói k...
Vladimir Popov
Mức độ

Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google! Phần 2

Xuất bản trong nhóm
Phần thứ hai của dự án - đây là liên kết đến phần đầu tiên: Và lớp BotState : Để bot của chúng tôi hiểu được những gì được mong đợi ở nó tại một thời điểm nhất định, chẳng hạn như xóa lời nhắc, chúng tôi cần phải bằng cách nào đó hãy cho bot của chúng tôi biết rằng các số được nhập và gửi bây giờ sẽ được coi là ID nhắc nhở khỏi danh sách và phải bị xóa. Do đó, sau khi nhấp vào nút “Xóa” , bot sẽ chuyển sang trạng thái BotState.ENTERNUMBEREVENT , đây là lớp Enum được tạo đặc biệt với các trạng thái 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
}
Và bây giờ chúng ta phải nhập số - “ Nhập số nhắc nhở từ danh sách .” Sau khi nhập họ sẽ đi đến phương pháp mong muốn để xử lý. Đây là chuyển đổi trạng thái của chúng tôi:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
Một bản đồ thông thường có ID người dùng và trạng thái của nó. Trường int adminId là dành cho tôi) Tiếp theo, logic của phương thức handUpdate sẽ kiểm tra xem đây là loại thông báo gì? Truy vấn gọi lại hay chỉ là văn bản? Nếu đây là văn bản thông thường thì chúng ta chuyển sang phương thức handInputMessage , trong đó chúng ta xử lý các nút menu chính và nếu chúng được nhấp vào thì chúng ta sẽ đặt trạng thái mong muốn, nhưng nếu chúng không được nhấp vào và đây là văn bản lạ thì chúng ta đặt trạng thái từ bộ nhớ đệm, nếu không có thì chúng ta đặt trạng thái bắt đầu. Sau đó văn bản sẽ đi vào xử lý phương thức xử lý với trạng thái mà chúng ta cần. Bây giờ chúng tôi trình bày logic của lớp MessageHandler , lớp này chịu trách nhiệm xử lý các tin nhắn tùy thuộc vào trạng thái của 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);
        }
    }
}
trong phương thức xử lý, chúng tôi kiểm tra trạng thái của tin nhắn chúng tôi nhận được và gửi nó đến trình xử lý sự kiện - lớp EventHandler. Ở đây chúng ta có hai lớp mới là MenuService và EventCash . MenuService – ở đây chúng tôi tạo tất cả các menu của mình. EventCash - tương tự như BotStateCash, nó sẽ lưu các phần sự kiện của chúng ta sau khi nhập và khi nhập xong, chúng ta sẽ lưu sự kiện vào cơ sở dữ liệu.
@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,>
Vâng, đó là. khi chúng tôi tạo một sự kiện, một đối tượng Sự kiện mới sẽ được tạo trong bộ đệm -eventCash.saveEventCash(userId, new Event()); Sau đó, chúng tôi nhập mô tả sự kiện và thêm nó vào bộ đệm:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Sau đó nhập số:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
Lớp CallbackQueryHandler tương tự như MessageHandler , chỉ có điều chúng ta xử lý các thông báo truy vấn gọi lại ở đó. Thật vô nghĩa khi phân tích hoàn toàn logic làm việc với các sự kiện - EventHandler , đã có quá nhiều chữ cái, có thể thấy rõ từ tên của các phương thức và nhận xét trong mã. Và tôi không thấy có ý nghĩa gì khi trình bày nó hoàn toàn bằng văn bản, có hơn 300 dòng. Đây là liên kết đến lớp học trên Github . Điều tương tự cũng xảy ra với lớp MenuService , nơi chúng ta tạo các menu của mình. Bạn có thể đọc chi tiết về chúng trên trang web của nhà sản xuất thư viện telegram - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Hoặc trong sách tham khảo Telegram - https:// tlgrm.ru/docs/bots /api Bây giờ chúng ta chỉ còn lại phần ngon nhất. Đây là lớp để xử lý các thông báo 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 – kích hoạt công việc theo lịch trình trong Spring. @Scheduled(cron = "0 0 0 * * *") – chúng tôi định cấu hình phương thức để chạy vào lúc 0:00 hàng ngày lịch.setTime(new Date()); - đặt thời gian máy chủ. Chúng tôi nhận được danh sách lời nhắc cho ngày hôm nay thông qua sự kỳ diệu của luồng và lambda. Chúng tôi xem qua danh sách đã nhận, đặt lịch thời gian gửi chính xácThời gian người dùng và... Đây là lúc tôi quyết định né tránh và khởi chạy các quy trình bị trì hoãn kịp thời. Lớp Time trong java chịu trách nhiệm về việc này . newTimer().schedule(new SimpleTask(sendEvent), CalendarUserTime.getTime()); Đối với nó, chúng ta cần tạo một chủ đề:
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);
    }
}
và triển khai TimeTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Vâng, tôi hoàn toàn hiểu rằng bạn có thể duyệt cơ sở dữ liệu 20 phút một lần và gửi tin nhắn, nhưng tôi đã viết mọi thứ về điều này ngay từ đầu)) Ở đây chúng ta cũng gặp phải nỗi khốn khổ của Heroku số 1. Với gói miễn phí, bạn được cấp khoảng 550 dino, tương đương với số giờ hoạt động của ứng dụng mỗi tháng. Số tiền này không đủ cho cả tháng hoạt động của ứng dụng, nhưng nếu bạn liên kết thẻ, bạn sẽ được tặng thêm 450 khủng long, đủ cho mắt bạn. Nếu bạn lo lắng về thẻ, bạn có thể liên kết một thẻ trống, nhưng hãy chắc chắn rằng nó chứa $0,6... Đây là số tiền xác minh, chỉ cần có trong tài khoản. Không có phí ẩn trừ khi bạn tự thay đổi biểu giá. Ở gói miễn phí, có một vấn đề nhỏ nữa, hãy gọi nó là số 1a.. Họ liên tục khởi động lại máy chủ hoặc đơn giản là gửi lệnh khởi động lại ứng dụng, nói chung là nó khởi động lại hàng ngày ở đâu đó vào lúc nửa đêm theo giờ Moscow và đôi khi vào những thời điểm khác. Từ đó, tất cả các quy trình trong bộ nhớ của chúng tôi sẽ bị xóa. Để giải quyết vấn đề này tôi đã nghĩ ra bảng EventCash. Trước khi gửi, các sự kiện được lưu trong một bảng riêng:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
Và sau khi gửi, những nội dung sau sẽ bị xóa:
@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 là một lớp đặc biệt để nhận ngữ cảnh một cách nhanh chóng:
@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;
    }
}
Để kiểm tra các sự kiện chưa được xử lý, tôi đã tạo một dịch vụ đặc biệt có phương thức được đánh dấu @PostConstruct - nó chạy sau mỗi lần khởi động ứng dụng. Nó chọn tất cả các sự kiện chưa được xử lý từ cơ sở dữ liệu và trả chúng vào bộ nhớ. Đây là một Heroku khó chịu dành cho bạn!
@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>
Ứng dụng của chúng ta đã sẵn sàng và đã đến lúc chúng ta lấy địa chỉ Heroku cho ứng dụng và cơ sở dữ liệu. Ứng dụng của bạn phải được xuất bản trên Github!!! Truy cập Heroku.com Nhấp vào Tạo ứng dụng mới , nhập tên ứng dụng của bạn, chọn Châu Âu, tạo ứng dụng . Thế là xong, nơi nộp đơn đã sẵn sàng. Nếu bạn nhấp vào mở Ứng dụng, trình duyệt sẽ chuyển hướng bạn đến địa chỉ ứng dụng của bạn, đây là địa chỉ webhook của bạn - https://your_name.herokuapp.com/ Đăng ký nó trong telegram và trong cài đặt của application.propertie thay đổi telegrambot. webHookPath=https: //telegrambotsimpl.herokuapp.com/ tới máy chủ của bạn.port=5000 có thể bị xóa hoặc nhận xét. Bây giờ hãy kết nối cơ sở dữ liệu. Đi tới tab Tài nguyên trên Heroku, nhấp vào: Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google!  Phần 2: - 1 Tìm Heroku Postgres ở đó , nhấp vào cài đặt : Bạn sẽ được chuyển hướng đến trang tài khoản cơ sở dữ liệu của mình. Tìm nó ở đó trong Cài đặt/ Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google!  Phần 2: - 2 Sẽ có tất cả dữ liệu cần thiết từ cơ sở dữ liệu của bạn. Trong application.properties bây giờ mọi thứ sẽ như thế này:
#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
Thay thế dữ liệu từ tài khoản của bạn bằng tài khoản của bạn: Trong trường jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org. postgresql.ssl .NonValidatingFactory cần được thay thế bằng chữ in đậm bằng dữ liệu tương ứng từ tài khoản (Host, Database), các trường tên người dùng, mật khẩu không khó đoán. Bây giờ chúng ta cần tạo các bảng trong cơ sở dữ liệu, tôi đã làm điều này từ IDEA. Tập lệnh của chúng tôi sẽ hữu ích cho việc tạo cơ sở dữ liệu. Chúng tôi thêm cơ sở dữ liệu như đã viết ở trên: Chúng tôi lấy Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google!  Phần 2: - 3 trường Máy chủ, Người dùng, Mật khẩu, Cơ sở dữ liệu từ tài khoản. Trường URl là trường spring.datasource.url của chúng tôi cho đến dấu chấm hỏi. Chúng ta chuyển đến tab Nâng cao , nó sẽ như thế này: Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google!  Phần 2: - 4 Nếu bạn đã làm đúng mọi thứ thì sau khi nhấp vào kiểm tra sẽ có dấu kiểm màu xanh lá cây. Bấm vào đồng ý. Nhấp chuột phải vào cơ sở dữ liệu của chúng tôi và chọn Chuyển tới bảng điều khiển truy vấn . Sao chép tập lệnh của chúng tôi ở đó và nhấp vào thực thi . Cơ sở dữ liệu nên được tạo ra. 10.000 dòng có sẵn miễn phí cho bạn! Mọi thứ đã sẵn sàng để triển khai. Đi tới ứng dụng của chúng tôi trên Heroku trong phần Triển khai. Chọn phần Github ở đó: Telegram bot - nhắc nhở qua webHook trong Java hoặc nói không với lịch Google!  Phần 2: - 5 Liên kết kho lưu trữ của bạn với Heroku. Bây giờ các nhánh của bạn sẽ được hiển thị. Đừng quên đẩy những thay đổi mới nhất của bạn vào .properties. Bên dưới, chọn nhánh sẽ được tải xuống và nhấp vào Triển khai nhánh . Nếu mọi thứ được thực hiện chính xác, bạn sẽ được thông báo rằng ứng dụng đã được triển khai thành công. Đừng quên bật Triển khai tự động từ .. Để ứng dụng của bạn tự động khởi động. Nhân tiện, khi bạn đẩy các thay đổi lên GitHub, Heroku sẽ tự động khởi động lại ứng dụng. Hãy cẩn thận về điều này, tạo một chủ đề riêng để bắt nạt và chỉ sử dụng chủ đề chính cho ứng dụng đang hoạt động. Bây giờ giá rẻ #2! Đây là nhược điểm nổi tiếng của gói miễn phí dành cho Heroku. Nếu không có tin nhắn đến, ứng dụng sẽ chuyển sang chế độ chờ và sau khi nhận được tin nhắn, ứng dụng sẽ khởi động khá lâu, điều này không hề dễ chịu. Có một giải pháp đơn giản cho việc này - https://uptimerobot.com/ Và không, các tiện ích ping của Google sẽ không giúp ích gì, tôi thậm chí còn không biết thông tin này đến từ đâu, tôi đã tìm kiếm câu hỏi này trên Google và trong khoảng 10 năm nay, chủ đề này chắc chắn không hoạt động, nếu nó có tác dụng. Ứng dụng này sẽ gửi các yêu cầu HEAD đến địa chỉ bạn chỉ định vào thời gian bạn đã đặt và nếu nó không phản hồi, hãy gửi tin nhắn qua email. Sẽ không khó để bạn tìm ra đâu, không có đủ nút bấm để nhầm lẫn đâu)) Xin chúc mừng!! Nếu tôi không quên bất cứ điều gì và bạn chú ý, thì bạn đã có ứng dụng của riêng mình hoạt động miễn phí và không bao giờ gặp sự cố. Cơ hội bắt nạt và thử nghiệm mở ra trước mắt bạn. Trong mọi trường hợp, tôi sẵn sàng trả lời các câu hỏi và chấp nhận mọi lời chỉ trích! Mã: https://github.com/papoff8295/webHookBotForHabr Tài liệu được sử dụng: https://tlgrm.ru/docs/bots/api - về bot. https://en.wikibooks.org/wiki/Java_Persistence - về các mối quan hệ trong cơ sở dữ liệu. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-special-time-in-java - Lớp thời gian và TimeTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – cách đăng mã trên Github https://github.com/rubenlagus/TelegramBots - thư viện telegram và rất nhiều thông tin hữu ích về nó.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION