La seconda parte del progetto - ecco un collegamento alla prima: E quindi la classe BotState : Affinché il nostro bot possa capire cosa ci si aspetta da lui in un determinato momento, ad esempio, eliminando un promemoria, dobbiamo in qualche modo fai sapere al nostro bot che i numeri inseriti e inviati ora dovrebbero essere trattati come ID promemoria dall'elenco e dovrebbero essere eliminati. Pertanto, dopo aver cliccato sul pulsante “Elimina” , il bot entra nello stato BotState.ENTERNUMBEREVENT , questa è una classe Enum appositamente creata con stati 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 ora dobbiamo inserire i numeri: " Inserisci il numero del promemoria dall'elenco ". Dopo aver inserito il quale andranno al metodo desiderato per l'elaborazione. Ecco il nostro cambio di stato:
public class BotStateCash {
private final Map<long, botstate=""> botStateMap = new HashMap<>();
public void saveBotState(long userId, BotState botState) {
botStateMap.put(userId, botState);
}
}
</long,>
Una mappa normale con un ID utente e il suo stato. Il campo int adminId è per me) Successivamente, la logica del metodo handleUpdate controllerà di che tipo di messaggio si tratta? Richiesta di richiamata o solo SMS? Se si tratta di testo normale, andiamo al metodo handleInputMessage , dove elaboriamo i pulsanti del menu principale e, se sono stati cliccati, impostiamo lo stato desiderato, ma se non sono stati cliccati e si tratta di testo sconosciuto, allora noi impostiamo lo stato dalla cache, se non è presente, impostiamo lo stato iniziale. Quindi il testo entra nell'elaborazione del metodo handle con lo stato di cui abbiamo bisogno. Ora presentiamo la logica della classe MessageHandler , che è responsabile dell'elaborazione dei messaggi in base allo stato 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);
}
}
}
nel metodo handle, controlliamo lo stato del messaggio che abbiamo ricevuto e lo inviamo al gestore di eventi, la classe EventHandler. Qui abbiamo due nuove classi, MenuService e EventCash . MenuService – qui creiamo tutti i nostri menu. EventCash - simile a BotStateCash, salverà parti del nostro evento dopo l'input e una volta completato l'input, salveremo l'evento nel database.
@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,>
Bene, questo è. quando creiamo un evento, un nuovo oggetto Evento viene creato nella cache -eventCash.saveEventCash(userId, new Event()); Quindi inseriamo una descrizione dell'evento e la aggiungiamo alla cache:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Quindi inserisci il numero:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
La classe CallbackQueryHandler è simile a MessageHandler , solo che lì elaboriamo i messaggi callbackquery. Non ha senso analizzare completamente la logica del lavoro con gli eventi - EventHandler , ci sono già troppe lettere, è chiaro dai nomi dei metodi e dai commenti nel codice. E non vedo il motivo di disporlo completamente nel testo, ci sono più di 300 righe. Ecco un collegamento alla lezione su Github . Lo stesso vale per la classe MenuService , dove creiamo i nostri menu. Puoi leggerli in dettaglio sul sito web del produttore della libreria di Telegram - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md O nel libro di riferimento di Telegram - https:// tlgrm.ru/docs/bots /api Ora ci rimane la parte più deliziosa. Questa è la classe per la gestione dei messaggi 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 : abilita il lavoro programmato in primavera. @Scheduled(cron = "0 0 0 * * *") – configuriamo il metodo per essere eseguito alle 0:00 ogni giorno calendar.setTime(new Date()); - imposta l'ora del server. Otteniamo un elenco di promemoria per oggi, attraverso la magia di stream e lambda. Esaminiamo l'elenco dei ricevuti, impostiamo l'orario di invio corretto calendarUserTime e... È qui che ho deciso di schivare e avviare i processi ritardati nel tempo. La classe Time in Java è responsabile di ciò . new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); Per questo dobbiamo creare un thread:
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 implementazione di TimerTask
public class SimpleTask extends TimerTask {
private final SendEvent sendEvent;
public SimpleTask(SendEvent sendEvent) {
this.sendEvent = sendEvent;
}
@Override
public void run() {
sendEvent.start();
}
}
Sì, capisco perfettamente che puoi consultare il database ogni 20 minuti e inviare messaggi, ma ho scritto tutto al riguardo all'inizio)) Qui incontriamo anche la miseria di Heroku n. 1. Nel piano gratuito ti vengono dati circa 550 dino, che sono qualcosa come le ore di funzionamento della tua applicazione al mese. Questo non è sufficiente per un mese intero di funzionamento dell'applicazione, ma se colleghi una carta, ti verranno dati altri 450 dino, che sono sufficienti per i tuoi occhi. Se sei preoccupato per la carta, puoi collegarne una vuota, ma assicurati che contenga $ 0,6... Questo è un importo di verifica, deve solo essere sul conto. Non ci sono costi nascosti a meno che tu non modifichi tu stesso la tariffa. Nel piano gratuito c'è un altro piccolo problema, chiamiamolo n. 1a.. Riavviano costantemente i server o semplicemente inviano un comando per riavviare l'applicazione, in generale si riavvia ogni giorno da qualche parte a mezzanotte, ora di Mosca, e talvolta in altri tempi. Da questo, tutti i nostri processi in memoria vengono cancellati. Per risolvere questo problema, ho ideato la tabella EventCash. Prima dell'invio, gli eventi vengono salvati in una tabella separata:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
E dopo l'invio, vengono eliminati:
@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 è una classe speciale per ottenere il contesto al volo:
@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;
}
}
Per verificare la presenza di eventi non elaborati, ho creato un servizio speciale che ha un metodo contrassegnato con @PostConstruct : viene eseguito dopo l'avvio di ogni applicazione. Raccoglie tutti gli eventi non elaborati dal database e li restituisce in memoria. Ecco un brutto Heroku per te!
@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>
La nostra applicazione è pronta ed è ora di ottenere l'indirizzo Heroku per l'applicazione e il database. La tua candidatura deve essere pubblicata su Github!!! Vai su Heroku.com Fai clic su Crea nuova app , inserisci il nome della tua applicazione, seleziona Europa, crea app . Questo è tutto, il posto per la domanda è pronto. Se fai clic su Apri app, il browser ti reindirizzerà all'indirizzo della tua applicazione, questo è il tuo indirizzo webhook - https://tuo_nome.herokuapp.com/ Registralo in telegram e nelle impostazioni di application.property cambia telegrambot. webHookPath=https: //telegrambotsimpl.herokuapp.com/ al tuo server.port=5000 può essere eliminato o commentato. Ora colleghiamo il database. Vai alla scheda Risorse su Heroku, fai clic su: Trova Heroku Postgres lì , fai clic su Installa : verrai reindirizzato alla pagina del tuo account database. Lo trovi lì in Impostazioni/ Ci saranno tutti i dati necessari dal tuo database. In application.properties ora tutto dovrebbe essere così:
#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
Sostituisci i dati del tuo account con i tuoi: Nel 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 deve essere sostituito in grassetto con i dati corrispondenti dell'account (Host, Database).I campi nome utente e password non sono difficili da indovinare. Ora dobbiamo creare le tabelle nel database, l'ho fatto da IDEA. Il nostro script sarà utile per creare un database. Aggiungiamo il database come scritto sopra: prendiamo dall'account il campo Host, Utente, Password, Database . Il campo URL è il nostro campo spring.datasource.url fino al punto interrogativo. Andiamo alla scheda Avanzate , dovrebbe essere così: Se hai fatto tutto correttamente, dopo aver fatto clic su test, ci sarà un segno di spunta verde. Fare clic su OK. Fare clic con il tasto destro sul nostro database e selezionare Passa alla console delle query . Copia lì il nostro script e fai clic su esegui . Il database dovrebbe essere creato. 10.000 linee sono a tua disposizione gratuitamente! Tutto è pronto per la distribuzione. Vai alla nostra applicazione su Heroku nella sezione Distribuisci. Seleziona la sezione Github lì: collega il tuo repository a Heroku. Ora i tuoi rami saranno visibili. Non dimenticare di inviare le ultime modifiche a .properties. Di seguito, seleziona il ramo che verrà scaricato e fai clic su Distribuisci ramo . Se tutto è stato eseguito correttamente, ti verrà notificato che l'applicazione è stata distribuita con successo. Non dimenticare di abilitare Distribuzioni automatiche da .. In modo che l'applicazione venga avviata automaticamente. A proposito, quando invii modifiche a GitHub, Heroku riavvierà automaticamente l'applicazione. Fai attenzione, crea un thread separato per il bullismo e utilizza quello principale solo per l'applicazione funzionante. Ora Economicità #2! Questo è il noto svantaggio del piano gratuito di Heroku. Se non ci sono messaggi in arrivo, l'applicazione entra in modalità standby e dopo aver ricevuto un messaggio ci vorrà molto tempo per avviarsi, il che non è piacevole. Esiste una soluzione semplice per questo: https://uptimerobot.com/ E no, i gadget ping di Google non aiutano, non so nemmeno da dove provengano queste informazioni, ho cercato su Google questa domanda e da circa 10 anni questo argomento non ha funzionato di sicuro, se ha funzionato del tutto. Questa applicazione invierà richieste HEAD all'indirizzo specificato per il tempo impostato e, se non risponde, invierà un messaggio via email. Non sarà difficile capirlo, non ci sono abbastanza pulsanti per confondersi)) Congratulazioni!! Se non ho dimenticato nulla e sei stato attento, allora hai la tua applicazione che funziona gratuitamente e non si blocca mai. L’opportunità di bullismo e sperimentazione si apre davanti a te. In ogni caso sono pronto a rispondere alle domande e ad accettare qualsiasi critica! Codice: https://github.com/papoff8295/webHookBotForHabr Materiali utilizzati: https://tlgrm.ru/docs/bots/api - sui bot. https://en.wikibooks.org/wiki/Java_Persistence - sulle relazioni nei database. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - Classe temporale e TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – come pubblicare codice su Github https://github.com/rubenlagus/TelegramBots - libreria di telegrammi e molte informazioni utili a riguardo.
GO TO FULL VERSION