Долбоордун экинчи бөлүгү - бул жерде биринчиге шилтеме : Ошентип, 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);
}
}
}
тутка ыкмасында биз алган билдирүүнүн абалын текшерип, аны окуяны иштетүүчүгө - EventHandler классына жөнөтөбүз. Бул жерде бизде эки жаңы класс бар, MenuService жана EventCash . MenuService – бул жерде биз бардык менюбузду түзөбүз. 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 an objectи түзүлөт -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 , буга чейин өтө көп тамгалар бар, бул codeдогу ыкмалардын жана комментарийлердин аталыштарынан көрүнүп турат. Ал эми мен аны толугу менен текстке түшүрүүнүн маанисин көрбөй турам, 300дөн ашык саптар бар. Бул жерде Githubдагы класска шилтеме . Ошол эле менюбузду түзгөн MenuService классына да тиешелүү . Алар жөнүндө кеңири маалыматты телеграмма китепканасынын өндүрүүчүсүнүн сайтынан окуй аласыз - 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 - жазында пландаштырылган ишти иштетүү. @Scheduled(cron = "0 0 0 * * *") – биз ыкманы күн сайын саат 0:00дө иштей тургандай конфигурациялайбыз calendar.setTime(жаңы Дата()); - server убактысын коюу. Биз агымдардын жана ламбданын сыйкырчылыгы аркылуу бүгүнкү күндө эстеткичтердин тизмесин алабыз. Биз кабыл алынган тизмеден өтүп, туура жөнөтүү убактысын коюңуз 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 мүнөт сайын маалымат базасына кирип, билдирүү жөнөтсө болоорун жакшы түшүнөм, бирок мен бул жөнүндө баарын башында эле жаздым)) Бул жерде биз №1 Герокунун азабына да туш болобуз. Акысыз планда сизге 550 дино берилет, бул сиздин колдонмоңуздун айына иштөө саатына окшош. Бул тиркеменин толук бир ай иштөөсү үчүн аздык кылат, бирок сиз картаны байланыштырсаңыз, сизге дагы 450 дино берилет, бул сиздин көзүңүз үчүн жетиштүү. Эгерде сиз картадан тынчсызданып жатсаңыз, анда сиз бош картаны байланыштырсаңыз болот, бирок анда $0,6 бар экенин текшериңиз... Бул текшерүү суммасы, ал жөн гана эсептин ичинде болушу керек. Тарифти өзүңүз алмаштырмайынча эч кандай жашыруун төлөмдөр жок. Акысыз планда дагы бир кичинекей көйгөй бар, аны №1а деп коёлу.. Алар serverлерди тынымсыз кайра жүктөшөт, же жөн гана тиркемени кайра жүктөө буйругун жөнөтүшөт, жалпысынан ал күн сайын Москва убактысы боюнча түн жарымында кайра жүктөлөт. башка убакта. Мындан биздин эс тутумдагы бардык процесстер жок кылынат. Бул көйгөйдү чечүү үчүн мен EventCash tableсын ойлоп таптым. Жөнөтүү алдында окуялар өзүнчө tableга сакталат:
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 сайтына өтүңүз. Жаңы колдонмо түзүү баскычын басыңыз , колдонмоңуздун атын киргизиңиз, Европаны тандаңыз, колдонмону түзүңүз . Болду, арыз үчүн жер даяр. Эгер сиз "Колдонмону ачуу" баскычын чыкылдатсаңыз, браузер сизди тиркемеңиздин дарегине багыттайт, бул сиздин вебхук дарегиңиз - https://your_name.herokuapp.com/ Аны телеграмда каттаңыз жана application.propertie нин жөндөөлөрүндө телеграмботту өзгөртүңүз . webHookPath=https: //telegrambotsimpl.herokuapp.com/ сиздин server.port=5000 жок кылса же комментарий калтырса болот. Эми базаны бириктирели. Heroku боюнча Ресурстар өтмөгүнө өтүңүз , чыкылдатыңыз: Ал жерден Heroku Postgres табыңыз , орнотууну басыңыз : Сиз маалымат базасынын эсебиңиздин барагына багытталасыз. Аны Орнотуулардан табыңыз/ Маалыматтар базасынан бардык керектүү маалыматтар болот. 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=requitory&sslf postgresql.ssl .NonValidatingFactory калың шрифт менен каттоо эсебинин (Хост, Берorштер базасы) тиешелүү маалыматтары менен алмаштырылышы керек. Колдонуучунун аты, сырсөз талааларын болжолдоо кыйын эмес. Эми биз базада tableларды түзүшүбүз керек, мен муну IDEAдан жасадым. Биздин скрипт маалымат базасын түзүү үчүн пайдалуу болот. Биз маалымат базасын жогоруда жазылгандай кошобуз: Каттоодон Хост , Колдонуучу, Сырсөз, Маалыматтар базасы талаасын алабыз. URl талаасы - бул суроо белгисине чейин биздин spring.datasource.url талаасы. Биз барабыз Өркүндөтүлгөн өтмөк , ал мындай болушу керек: Эгер сиз бардыгын туура кылган болсоңуз, анда тестти басканда жашыл белги пайда болот. OK басыңыз. Биздин базабызды оң баскыч менен чыкылдатып, суроо консолуна өтүүнү тандаңыз . Биздин скриптти ошол жерге көчүрүп, аткарууну басыңыз . Маалымат базасы түзүлүшү керек. 10 000 линия сизге бекер жеткorктүү! Баары Жайгаштыруу үчүн даяр. Жайгаштыруу бөлүмүндөгү Heroku колдонмосуна өтүңүз. Ал жерден Github бөлүмүн тандаңыз: Репозиторийиңизди Heroku менен байланыштырыңыз. Эми бутактарыңыз көрүнүп калат. Акыркы өзгөртүүлөрүңүздү .properties'ге түртүүнү унутпаңыз. Төмөндө, жүктөлө турган бутакты тандап, Бутакты жайгаштыруу баскычын басыңыз . Эгер баары туура аткарылса, сиз колдонмо ийгorктүү орнотулганы жөнүндө кабар аласыз. Колдонмоңуз автоматтык түрдө ишке кириши үчүн ..дан Автоматтык жайылтууларды иштетүүнү унутпаңыз . Айтмакчы, сиз GitHub'ка өзгөртүүлөрдү түрткөнүңүздө, Heroku колдонмону автоматтык түрдө өчүрүп күйгүзөт. Бул жөнүндө сак болуңуз, коркутуу үчүн өзүнчө жип түзүңүз жана негизгисин жумушчу колдонмо үчүн гана колдонуңуз. Азыр арзандык №2! Бул Heroku үчүн акысыз пландын белгилүү кемчorги. Эгерде кирүүчү билдирүүлөр жок болсо, тиркеме күтүү режимине өтөт жана кабарды алгандан кийин баштоо үчүн бир топ убакыт талап кылынат, бул жагымдуу эмес. Бул үчүн жөнөкөй чечим бар - https://uptimerobot.com/ Жок, Google пинг гаджеттери жардам бербейт, мен бул маалымат кайдан келгенин да билбейм, мен бул суроону 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 боюнча codeду кантип жайгаштыруу керек - телеграмма китепканасы жана ал жөнүндө көптөгөн пайдалуу маалымат.
GO TO FULL VERSION