JavaRush /Java blogi /Random-UZ /Telegram boti - Java-dagi webHook orqali eslatma yoki Goo...

Telegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng! 2-qism

Guruhda nashr etilgan
Loyihaning ikkinchi qismi - bu erda birinchisiga havola : Shunday qilib, BotState klassi : Bizning botimiz ma'lum bir vaqtda undan nima kutilayotganini tushunishi uchun, masalan, eslatmani o'chirish, qandaydir tarzda bizning botimizga raqamlar kiritilgan va yuborilganligi haqida xabar bering. Shuning uchun, "O'chirish" tugmasini bosgandan so'ng , bot BotState.ENTERNUMBEREVENT holatiga o'tadi , bu bot holatlari bilan maxsus yaratilgan Enum sinfidir.
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
}
Endi biz raqamlarni kiritishimiz kutilmoqda - " Ro'yxatdagi eslatma raqamini kiriting ." Kiritgandan so'ng, ular kerakli ishlov berish usuliga o'tadilar. Mana bizning davlat kalitimiz:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
Foydalanuvchi identifikatori va uning holati bilan oddiy xarita. int adminId maydoni men uchun) Keyinchalik, handleUpdate usulining mantig'i bu qanday xabar ekanligini tekshiradi? Qayta qo'ng'iroq qilishmi yoki shunchaki matnmi? Agar bu oddiy matn bo'lsa, biz handleInputMessage usuliga o'tamiz , u erda biz asosiy menyu tugmachalarini qayta ishlaymiz va agar ular bosilsa, biz kerakli holatni o'rnatamiz, lekin agar ular bosilmasa va bu notanish matn bo'lsa, biz holatni keshdan o'rnating, agar u mavjud bo'lmasa, biz boshlang'ich holatni o'rnatamiz. Keyin matn bizga kerak bo'lgan holat bilan ishlov berish usulini qayta ishlashga o'tadi. Endi biz bot holatiga qarab xabarlarni qayta ishlash uchun mas'ul bo'lgan MessageHandler sinfining mantiqini taqdim etamiz :
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);
        }
    }
}
Tutqich usulida biz olingan xabarning holatini tekshiramiz va uni voqea ishlovchisi - EventHandler sinfiga yuboramiz. Bu erda ikkita yangi sinf mavjud, MenuService va EventCash . MenuService - bu erda biz barcha menyularimizni yaratamiz. EventCash - BotStateCash-ga o'xshash , u kiritilgandan so'ng voqeamizning qismlarini saqlaydi va kiritish tugallangandan so'ng biz voqeani ma'lumotlar bazasida saqlaymiz.
@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,>
Xo'sh, bu. biz voqea yaratganimizda keshda yangi Event ob'ekti yaratiladi -eventCash.saveEventCash(userId, new Event()); Keyin biz voqea tavsifini kiritamiz va uni keshga qo'shamiz:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Keyin raqamni kiriting:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
CallbackQueryHandler klassi MessageHandler ga o'xshaydi , faqat biz u erda qayta qo'ng'iroq qilish xabarlarini qayta ishlaymiz. Voqealar bilan ishlash mantiqini to'liq tahlil qilish mantiqiy emas - EventHandler , allaqachon juda ko'p harflar mavjud, koddagi usullar va sharhlarning nomlaridan aniq. Va men uni to'liq matnda joylashtirishning ma'nosini ko'rmayapman, 300 dan ortiq satrlar mavjud. Bu erda Github'dagi sinfga havola . Xuddi shu narsa MenuService klassi uchun ham amal qiladi , bu erda biz menyularimizni yaratamiz. Siz ular haqida batafsil telegram kutubxonasi ishlab chiqaruvchisi veb-saytida o'qishingiz mumkin - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Yoki Telegram ma'lumotnomasida - https:// tlgrm.ru/docs/bots /api Endi bizda eng mazali qism qoldi. Bu EventService xabarlarini qayta ishlash sinfi :
@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 - bahorda rejalashtirilgan ishni yoqish. @Scheduled(cron = "0 0 0 * * *") – biz usulni har kuni soat 0:00 da ishga tushirish uchun sozlaymiz calendar.setTime(yangi Sana()); - server vaqtini belgilash. Biz oqimlar va lambda sehrlari orqali bugungi kun uchun eslatmalar ro'yxatini olamiz. Biz qabul qilingan ro'yxatni ko'rib chiqamiz, to'g'ri jo'natish vaqtini o'rnatamiz kalendarUserTime va... Bu erda men o'z vaqtida kechiktirilgan jarayonlarni chetlab o'tishga qaror qildim. Buning uchun java'dagi Time klassi javobgardir . new Timer().schedule(yangi SimpleTask(sendEvent), calendarUserTime.getTime()); Buning uchun biz ip yaratishimiz kerak:
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);
    }
}
va TimerTaskni amalga oshirish
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Ha, men har 20 daqiqada ma'lumotlar bazasidan o'tib, xabar yuborishingiz mumkinligini juda yaxshi tushunaman, lekin men bu haqda hamma narsani boshida yozganman)) Bu erda biz 1-sonli Herokuning baxtsizligiga duch kelamiz. Bepul rejada sizga taxminan 550 dino beriladi, bu sizning arizangiz oyiga ishlash soatlariga o'xshaydi. Ilovaning to'liq oylik ishlashi uchun bu etarli emas, lekin agar siz kartani bog'lasangiz, sizga yana 450 dino beriladi, bu sizning ko'zingiz uchun etarli. Agar siz kartadan xavotirda bo'lsangiz, siz bo'sh kartani bog'lashingiz mumkin, lekin unda 0,6 dollar borligiga ishonch hosil qiling ... Bu tekshirish miqdori, u faqat hisobda bo'lishi kerak. Tarifni o'zingiz o'zgartirmasangiz, hech qanday yashirin to'lovlar yo'q. Bepul rejada yana bitta kichik muammo bor, keling, uni № 1a deb ataymiz.. Ular doimiy ravishda serverlarni qayta ishga tushirishadi yoki oddiygina dasturni qayta ishga tushirish uchun buyruq yuborishadi, umuman olganda, u har kuni Moskva vaqti bilan yarim tunda bir joyda qayta ishga tushadi va ba'zan. boshqa paytlarda. Bundan bizning xotiramizdagi barcha jarayonlar o'chiriladi. Ushbu muammoni hal qilish uchun men EventCash jadvalini o'ylab topdim. Yuborishdan oldin voqealar alohida jadvalda saqlanadi:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
Va yuborilgandan so'ng, quyidagilar o'chiriladi:
@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 kontekstni tezda olish uchun maxsus sinfdir:
@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;
    }
}
Ishlov berilmagan hodisalarni tekshirish uchun men @PostConstruct deb belgilangan usulga ega bo'lgan maxsus xizmatni yaratdim - u har bir dastur boshlangandan keyin ishlaydi. U ma'lumotlar bazasidan qayta ishlanmagan barcha hodisalarni oladi va ularni xotiraga qaytaradi. Mana siz uchun yomon 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>
Bizning arizamiz tayyor va dastur va ma'lumotlar bazasi uchun Heroku manzilini olish vaqti keldi. Sizning arizangiz Github-da e'lon qilinishi kerak!!! Heroku.com saytiga o‘ting Yangi ilova yaratish ni bosing , dastur nomini kiriting, Yevropani tanlang, ilova yarating . Bo‘ldi, ariza topshirish uchun joy tayyor. Agar siz Ilovani ochish tugmasini bossangiz, brauzer sizni ilovangiz manziliga yoʻnaltiradi, bu sizning webhook manzilingiz - https://your_name.herokuapp.com/ Uni telegramda roʻyxatdan oʻtkazing va application.propertie sozlamalarida telegrambotni oʻzgartiring . webHookPath=https: //telegrambotsimpl.herokuapp.com/ server.port =5000 manzilini oʻchirib tashlash yoki sharhlash mumkin. Endi ma'lumotlar bazasini bog'laymiz. Heroku-dagi Resurslar yorlig'iga o'ting , bosing: U erda Heroku Postgres-niTelegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng!  2-qism: - 1 toping , o'rnatish-ni bosing : Siz ma'lumotlar bazasi hisob qaydnomangiz sahifasiga yo'naltirilasiz. Uni Sozlamalarda toping/ Ma'lumotlar bazasidan barcha kerakli ma'lumotlar bo'ladi. Application.properties da hamma narsa endi shunday bo'lishi kerak: Telegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng!  2-qism: - 2
#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
Hisobingizdagi ma'lumotlarni o'zingiz bilan almashtiring: jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=requitory&sslf maydonida. postgresql.ssl .NonValidatingFactory qalin harflar bilan almashtirilishi kerak, hisob qaydnomasidan tegishli ma'lumotlar (xost, ma'lumotlar bazasi) Foydalanuvchi nomi, parol maydonlarini taxmin qilish qiyin emas. Endi biz ma'lumotlar bazasida jadvallar yaratishimiz kerak, men buni IDEA'dan qildim. Bizning skriptimiz ma'lumotlar bazasini yaratish uchun foydali bo'ladi. Biz ma'lumotlar bazasini yuqorida yozilganidek qo'shamiz: Hisobdan Telegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng!  2-qism: - 3 Xost , Foydalanuvchi, Parol, Ma'lumotlar bazasi maydonini olamiz. URl maydoni - bu savol belgisigacha bo'lgan spring.datasource.url maydonimiz. Biz o'tamiz Kengaytirilgan yorlig'i , u shunday bo'lishi kerak: Telegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng!  2-qism: - 4 Agar siz hamma narsani to'g'ri bajargan bo'lsangiz, testni bosgandan so'ng yashil tasdiq belgisi bo'ladi. OK tugmasini bosing. Ma'lumotlar bazasini sichqonchaning o'ng tugmasi bilan bosing va so'rov konsoliga o'tish- ni tanlang . Bizning skriptimizni u yerga nusxa ko'chiring va bajarish- ni bosing . Ma'lumotlar bazasi yaratilishi kerak. 10 000 ta qator siz uchun bepul! Deploy uchun hamma narsa tayyor. Joylashtirish bo'limida Heroku ilovamizga o'ting. U erda Github bo'limini tanlang: Telegram boti - Java-dagi webHook orqali eslatma yoki Google kalendariga yo'q deng!  2-qism: - 5 O'z omboringizni Heroku bilan bog'lang. Endi sizning filiallaringiz ko'rinadi. So'nggi o'zgarishlarni .properties ga o'tkazishni unutmang. Quyida yuklab olinadigan filialni tanlang va Filialni joylashtirish tugmasini bosing . Agar hamma narsa to'g'ri bajarilgan bo'lsa, dastur muvaffaqiyatli joylashtirilganligi haqida xabar olasiz. Ilovangiz avtomatik ravishda ishga tushishi uchun ..dan Avtomatik joylashtirishni yoqishni unutmang . Aytgancha, siz GitHub-ga o'zgartirishlarni kiritganingizda, Heroku dasturni avtomatik ravishda qayta ishga tushiradi. Bu haqda ehtiyot bo'ling, bezorilik uchun alohida mavzu yarating va asosiysini faqat ishlaydigan dastur uchun ishlating. Endi arzonlik №2! Bu Heroku uchun bepul rejaning taniqli kamchiligi. Agar kiruvchi xabarlar bo'lmasa, dastur kutish rejimiga o'tadi va xabarni olgandan so'ng uni boshlash uchun ancha vaqt kerak bo'ladi, bu yoqimli emas. Buning oddiy yechimi bor - https://uptimerobot.com/ Va yo'q, Google ping gadjetlari yordam bermaydi, men bu ma'lumot qaerdan kelganini ham bilmayman, men bu savolni Google'da qidirdim va taxminan 10 yil davomida bu mavzu aniq ishlamayapti, agar u umuman ishlagan bo'lsa. Ushbu ilova siz belgilagan vaqt uchun HEAD so'rovlarini yuboradi va agar javob bermasa, elektron pochta orqali xabar yuboradi. Buni tushunish siz uchun qiyin bo'lmaydi, adashtirish uchun tugmalar etarli emas)) Tabriklaymiz!! Agar men hech narsani unutmagan bo'lsam va siz diqqatli bo'lsangiz, unda sizda bepul ishlaydigan va hech qachon ishdan chiqadigan o'z ilovangiz bor. Sizning oldingizda qo'rqitish va tajriba o'tkazish imkoniyati ochiladi. Har qanday holatda ham savollarga javob berishga va har qanday tanqidni qabul qilishga tayyorman! Kod: https://github.com/papoff8295/webHookBotForHabr Ishlatilgan materiallar: https://tlgrm.ru/docs/bots/api - botlar haqida. https://en.wikibooks.org/wiki/Java_Persistence - ma'lumotlar bazalaridagi munosabatlar haqida. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - Vaqt sinfi va TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – kodni Github-ga qanday joylashtirish mumkin https://github.com/rubenlagus/TelegramBots - telegram kutubxonasi va u haqida juda ko'p foydali ma'lumotlar.
Izohlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION