JavaRush /مدونة جافا /Random-AR /روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقوي...
Vladimir Popov
مستوى

روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google! الجزء 2

نشرت في المجموعة
الجزء الثاني من المشروع - هنا رابط للجزء الأول: وهكذا فئة BotState : لكي يفهم الروبوت الخاص بنا ما هو متوقع منه في وقت معين، على سبيل المثال، حذف تذكير، نحتاج إلى بطريقة أو بأخرى، أخبر الروبوت الخاص بنا أن الأرقام التي تم إدخالها وإرسالها الآن يجب أن يتم التعامل معها كمعرف تذكير من القائمة ويجب حذفها. لذلك، بعد النقر فوق الزر "حذف" ، ينتقل الروبوت إلى حالة 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,>
خريطة عادية مع معرف المستخدم وحالته. حقل int adminId مخصص لي) بعد ذلك، سيتحقق منطق طريقة HandleUpdate من نوع هذه الرسالة؟ Callbackquery أم مجرد رسالة نصية؟ إذا كان هذا نصًا عاديًا، فإننا ننتقل إلى طريقة 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,>
حسنا، هذا هو. عندما نقوم بإنشاء حدث، يتم إنشاء كائن حدث جديد في ذاكرة التخزين المؤقت -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 ، ونحن فقط نقوم بمعالجة رسائل callbackquery هناك. ليس من المنطقي إجراء تحليل كامل لمنطق العمل مع الأحداث - EventHandler ، فهناك بالفعل عدد كبير جدًا من الحروف، وهذا واضح من أسماء الطرق والتعليقات في الكود. ولا أرى أي فائدة من وضعها بالكامل في النص، فهناك أكثر من 300 سطر. هنا رابط للفصل على جيثب . الأمر نفسه ينطبق على فئة MenuService ، حيث نقوم بإنشاء قوائمنا. يمكنك أن تقرأ عنها بالتفصيل على الموقع الإلكتروني للشركة المصنعة لمكتبة Telegram - 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(new Date()); - ضبط وقت الخادم. نحصل على قائمة التذكيرات لهذا اليوم، من خلال سحر التدفقات ولامدا. نستعرض القائمة المستلمة ونضبط وقت الإرسال الصحيح CalendarUserTime و... هذا هو المكان الذي قررت فيه المراوغة وبدء العمليات المتأخرة في الوقت المناسب. فئة الوقت في Java هي المسؤولة عن هذا . 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 دقيقة وإرسال الرسائل، لكنني كتبت كل شيء عن هذا في البداية)) هنا نواجه أيضًا بؤس Heroku رقم 1. في الخطة المجانية، يتم منحك حوالي 550 دينو، وهو ما يشبه ساعات تشغيل تطبيقك شهريًا. وهذا لا يكفي لمدة شهر كامل من تشغيل التطبيق، ولكن إذا قمت بربط البطاقة، يتم منحك 450 دينو أخرى، وهو ما يكفي لعينيك. إذا كنت قلقًا بشأن البطاقة، يمكنك ربط بطاقة فارغة، ولكن تأكد من أنها تحتوي على 0.6 دولار... هذا مبلغ تحقق، يجب أن يكون في الحساب فقط. لا توجد رسوم مخفية إلا إذا قمت بتغيير التعرفة بنفسك. في الخطة المجانية، هناك مشكلة صغيرة أخرى، دعنا نسميها رقم 1 أ.. يقومون بإعادة تشغيل الخوادم باستمرار، أو ببساطة يرسلون أمرًا لإعادة تشغيل التطبيق، بشكل عام، يتم إعادة التشغيل كل يوم في مكان ما في منتصف الليل بتوقيت موسكو، وأحيانًا في أوقات أخرى. من هذا، يتم حذف جميع عملياتنا في الذاكرة. لحل هذه المشكلة، توصلت إلى جدول EventCash. قبل الإرسال، يتم حفظ الأحداث في جدول منفصل:
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 للتطبيق وقاعدة البيانات. يجب نشر طلبك على جيثب !!! انتقل إلى Heroku.com، وانقر فوق "إنشاء تطبيق جديد" ، وأدخل اسم التطبيق الخاص بك، وحدد " أوروبا"، ثم أنشئ التطبيق . هذا كل شيء، المكان المخصص للتطبيق جاهز. إذا قمت بالنقر فوق فتح التطبيق، فسيقوم المتصفح بإعادة توجيهك إلى عنوان تطبيقك، وهذا هو عنوان خطاف الويب الخاص بك - https://your_name.herokuapp.com/ قم بتسجيله في telegram، وفي إعدادات application.propertie قم بتغيير telegrambot. webHookPath=https: //telegrambotsimpl.herokuapp.com/ إلى الخادم الخاص بك.port=5000 يمكن حذفه أو التعليق عليه. الآن دعونا نربط قاعدة البيانات. انتقل إلى علامة التبويب "الموارد" في Heroku، وانقر فوق: روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google!  الجزء الثاني :- 1 ابحث عن Heroku Postgres هناك ، وانقر فوق "تثبيت" : ستتم إعادة توجيهك إلى صفحة حساب قاعدة البيانات الخاصة بك. يمكنك العثور عليه هناك في الإعدادات/ روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google!  الجزء الثاني :- 2 ستكون هناك جميع البيانات الضرورية من قاعدة البيانات الخاصة بك. في 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=require&sslfactory=org. يجب استبدال postgresql.ssl .NonValidatingFactory بالخط العريض بالبيانات المقابلة من الحساب (المضيف، قاعدة البيانات).ليس من الصعب تخمين حقول اسم المستخدم وكلمة المرور. الآن نحن بحاجة إلى إنشاء الجداول في قاعدة البيانات، فعلت ذلك من IDEA. سيكون البرنامج النصي الخاص بنا مفيدًا لإنشاء قاعدة بيانات. نضيف قاعدة البيانات كما هي مكتوبة أعلاه: نأخذ روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google!  الجزء الثاني :- 3 حقل المضيف والمستخدم وكلمة المرور وقاعدة البيانات من الحساب. حقل URL هو حقل Spring.datasource.url الخاص بنا حتى علامة الاستفهام. نذهب إلى علامة التبويب خيارات متقدمة ، وينبغي أن يكون مثل هذا: روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google!  الجزء الثاني :- 4 إذا فعلت كل شيء بشكل صحيح، فبعد النقر على الاختبار، ستكون هناك علامة اختيار خضراء. انقر فوق موافق. انقر بزر الماوس الأيمن على قاعدة البيانات الخاصة بنا وحدد الانتقال إلى وحدة تحكم الاستعلام . انسخ البرنامج النصي الخاص بنا هناك وانقر فوق "تنفيذ" . ينبغي إنشاء قاعدة البيانات. 10.000 خط متاحة لك مجانًا! كل شيء جاهز للنشر. انتقل إلى تطبيقنا على Heroku في قسم النشر. حدد قسم Github هناك: روبوت Telegram - تذكير عبر webHook في Java أو قل لا لتقويم Google!  الجزء الثاني :- 5 اربط مستودعك بـ Heroku. الآن سوف تكون فروعك مرئية. لا تنس أن ترسل آخر تغييراتك إلى ‎.properties. أدناه، حدد الفرع الذي سيتم تنزيله وانقر فوق نشر الفرع . إذا تم كل شيء بشكل صحيح، فسيتم إعلامك بأنه تم نشر التطبيق بنجاح. لا تنس تمكين النشر التلقائي من .. حتى يبدأ تطبيقك تلقائيًا. بالمناسبة، عندما تقوم بإدخال التغييرات على GitHub، سيقوم Heroku بإعادة تشغيل التطبيق تلقائيًا. كن حذرًا بشأن هذا، وقم بإنشاء موضوع منفصل للتنمر، واستخدم الموضوع الرئيسي فقط لتطبيق العمل. الآن الرخص رقم 2! هذا هو العيب المعروف للخطة المجانية لـ Heroku. إذا لم تكن هناك رسائل واردة، فسينتقل التطبيق إلى وضع الاستعداد، وبعد تلقي الرسالة، سيستغرق البدء وقتًا طويلاً، وهو أمر غير لطيف. هناك حل بسيط لهذا - https://uptimerobot.com/ لا، لن تساعد أدوات Google ping، ولا أعرف حتى من أين جاءت هذه المعلومات، لقد بحثت في 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-speci-time-in-java - فئة الوقت وTimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 - كيفية نشر الكود على Github https://github.com/rubenlagus/TelegramBots - مكتبة التليجرام والكثير من المعلومات المفيدة حول هذا الموضوع.
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION