JavaRush /Java блогу /Random-KY /Telegram боту - Java'дагы webHook аркылуу эскертме же Goo...
Vladimir Popov
Деңгээл

Telegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт! 2 бөлүк

Группада жарыяланган
Долбоордун экинчи бөлүгү - бул жерде биринчиге шилтеме : Ошентип, 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 PostgresTelegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт!  2-бөлүк: - 1 табыңыз , орнотууну басыңыз : Сиз маалымат базасынын эсебиңиздин барагына багытталасыз. Аны Орнотуулардан табыңыз/ Маалыматтар базасынан бардык керектүү маалыматтар болот. application.properties ичинде баары азыр мындай болушу керек: Telegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт!  2-бөлүк: - 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
Каттоо эсебиңиздеги дайындарды өзүңүздүкү менен алмаштырыңыз: талаада 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дан жасадым. Биздин скрипт маалымат базасын түзүү үчүн пайдалуу болот. Биз маалымат базасын жогоруда жазылгандай кошобуз: Каттоодон Telegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт!  2-бөлүк: - 3 Хост , Колдонуучу, Сырсөз, Маалыматтар базасы талаасын алабыз. URl талаасы - бул суроо белгисине чейин биздин spring.datasource.url талаасы. Биз барабыз Өркүндөтүлгөн өтмөк , ал мындай болушу керек: Telegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт!  2-бөлүк: - 4 Эгер сиз бардыгын туура кылган болсоңуз, анда тестти басканда жашыл белги пайда болот. OK басыңыз. Биздин базабызды оң баскыч менен чыкылдатып, суроо консолуна өтүүнү тандаңыз . Биздин скриптти ошол жерге көчүрүп, аткарууну басыңыз . Маалымат базасы түзүлүшү керек. 10 000 линия сизге бекер жеткorктүү! Баары Жайгаштыруу үчүн даяр. Жайгаштыруу бөлүмүндөгү Heroku колдонмосуна өтүңүз. Ал жерден Github бөлүмүн тандаңыз: Telegram боту - Java'дагы webHook аркылуу эскертме же Google календарына жок деп айт!  2-бөлүк: - 5 Репозиторийиңизди 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ду кантип жайгаштыруу керек - телеграмма китепканасы жана ал жөнүндө көптөгөн пайдалуу маалымат.
Комментарийлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION