JavaRush /Java-Blog /Random-DE /Telegram-Bot – Erinnerung per WebHook in Java oder Nein z...
Vladimir Popov
Level 41

Telegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender! Teil 2

Veröffentlicht in der Gruppe Random-DE
Der zweite Teil des Projekts – hier ist ein Link zum ersten: Und so die BotState- Klasse : Damit unser Bot versteht, was zu einem bestimmten Zeitpunkt von ihm erwartet wird, beispielsweise das Löschen einer Erinnerung, müssen wir dies tun Teilen Sie unserem Bot irgendwie mit, dass die eingegebenen und gesendeten Nummern jetzt als Erinnerungs-ID aus der Liste behandelt und gelöscht werden sollten. Daher wechselt der Bot nach dem Klicken auf die Schaltfläche „Löschen“ in den Status BotState.ENTERNUMBEREVENT . Dies ist eine speziell erstellte Enum-Klasse mit Bot-Zuständen.
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
}
Und nun müssen wir Zahlen eingeben: „ Geben Sie die Erinnerungsnummer aus der Liste ein .“ Nach der Eingabe gelangen sie zur gewünschten Bearbeitungsmethode. Hier ist unser Statusschalter:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
Eine reguläre Karte mit einer Benutzer-ID und ihrem Status. Das int adminId- Feld ist für mich.) Als nächstes prüft die Logik der handleUpdate- Methode, um welche Art von Nachricht es sich handelt. Rückrufanfrage oder nur SMS? Wenn es sich um normalen Text handelt, gehen wir zur Methode handleInputMessage , wo wir die Hauptmenüschaltflächen verarbeiten. Wenn sie angeklickt wurden, stellen wir den gewünschten Status ein. Wenn sie jedoch nicht angeklickt wurden und es sich um unbekannten Text handelt, dann wir Legen Sie den Status aus dem Cache fest. Wenn er nicht vorhanden ist, legen wir den Startstatus fest. Anschließend verarbeitet der Text die Handle-Methode mit dem von uns benötigten Status. Jetzt stellen wir die Logik der MessageHandler- Klasse vor , die für die Verarbeitung von Nachrichten abhängig vom Status des Bots verantwortlich ist:
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);
        }
    }
}
In der Handle-Methode prüfen wir den Status der empfangenen Nachricht und senden sie an den Event-Handler – die EventHandler-Klasse. Hier haben wir zwei neue Klassen, MenuService und EventCash . MenuService – hier erstellen wir alle unsere Menüs. EventCash – ähnlich wie BotStateCash speichert es Teile unseres Ereignisses nach der Eingabe und wenn die Eingabe abgeschlossen ist, speichern wir das Ereignis in der Datenbank.
@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,>
Nun ja, das ist. Wenn wir ein Ereignis erstellen, wird ein neues Ereignisobjekt im Cache erstellt -eventCash.saveEventCash(userId, new Event()); Dann geben wir eine Beschreibung des Ereignisses ein und fügen sie dem Cache hinzu:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Geben Sie dann die Nummer ein:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
Die CallbackQueryHandler-Klasse ähnelt MessageHandler , nur verarbeiten wir dort Callbackquery-Nachrichten. Es macht keinen Sinn , die Logik der Arbeit mit Ereignissen vollständig zu analysieren - EventHandler , es gibt bereits zu viele Buchstaben, das geht aus den Namen der Methoden und Kommentaren im Code hervor. Und ich sehe keinen Sinn darin, es komplett in Textform darzustellen, es sind mehr als 300 Zeilen. Hier ist ein Link zum Kurs auf Github . Das Gleiche gilt für die MenuService- Klasse , in der wir unsere Menüs erstellen. Sie können sie ausführlich auf der Website des Telegram-Bibliotheksherstellers lesen – https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Oder im Telegram-Nachschlagewerk – https:// tlgrm.ru/docs/bots /api Jetzt bleibt uns der köstlichste Teil. Dies ist die Klasse für die Verarbeitung von EventService- Nachrichten :
@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 – Geplante Arbeit im Frühjahr aktivieren. @Scheduled(cron = "0 0 0 * * *") – wir konfigurieren die Methode so, dass sie jeden Tag um 0:00 Uhr ausgeführt wird . Calendar.setTime(new Date()); - Serverzeit einstellen. Durch die Magie von Streams und Lambda erhalten wir eine Liste mit Erinnerungen für heute. Wir gehen die empfangene Liste durch, stellen die korrekte Sendezeit ein, CalendarUserTime und... Hier habe ich beschlossen, auszuweichen und die Prozesse zeitlich verzögert zu starten. Verantwortlich dafür ist die Time- Klasse in Java . new Timer().schedule(new SimpleTask(sendEvent), CalendarUserTime.getTime()); Dafür müssen wir einen Thread erstellen:
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);
    }
}
und Implementierung von TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
Ja, ich verstehe vollkommen gut, dass man alle 20 Minuten die Datenbank durchgehen und Nachrichten verschicken kann, aber ich habe alles dazu ganz am Anfang geschrieben)) Hier stoßen wir auch auf die Misere von Heroku Nr. 1. Beim kostenlosen Plan erhalten Sie etwa 550 Dino, was in etwa den Betriebsstunden Ihrer Anwendung pro Monat entspricht. Dies reicht nicht für einen ganzen Monat Betrieb der Anwendung, aber wenn Sie eine Karte verknüpfen, erhalten Sie weitere 450 Dino, was für Ihre Augen ausreicht. Wenn Sie sich Sorgen um die Karte machen, können Sie eine leere Karte verknüpfen, aber stellen Sie sicher, dass sie 0,6 $ enthält... Dies ist ein Verifizierungsbetrag, er muss nur auf dem Konto vorhanden sein. Es gibt keine versteckten Kosten, es sei denn, Sie ändern den Tarif selbst. Beim kostenlosen Plan gibt es noch ein kleines Problem, nennen wir es Nr. 1a. Sie starten die Server ständig neu oder senden einfach einen Befehl zum Neustart der Anwendung. Im Allgemeinen wird sie jeden Tag irgendwo um Mitternacht Moskauer Zeit und manchmal auch neu gestartet zu anderen Zeiten. Dadurch werden alle unsere Prozesse im Speicher gelöscht. Um dieses Problem zu lösen, habe ich mir die EventCash-Tabelle ausgedacht. Vor dem Senden werden Ereignisse in einer separaten Tabelle gespeichert:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
Und nach dem Absenden werden gelöscht:
@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 ist eine spezielle Klasse zum schnellen Abrufen von Kontext:
@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;
    }
}
Um nach unverarbeiteten Ereignissen zu suchen, habe ich einen speziellen Dienst erstellt, der über eine Methode mit der Bezeichnung @PostConstruct verfügt – sie wird nach jedem Anwendungsstart ausgeführt. Es holt alle unverarbeiteten Ereignisse aus der Datenbank und gibt sie an den Speicher zurück. Hier ist ein fieser Heroku für dich!
@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>
Unsere Anwendung ist fertig und es ist Zeit für uns, die Heroku-Adresse für die Anwendung und die Datenbank zu erhalten. Ihre Bewerbung muss auf Github veröffentlicht werden!!! Gehen Sie zu Heroku.com. Klicken Sie auf „ Neue App erstellen“ , geben Sie Ihren Anwendungsnamen ein, wählen Sie „ Europa“ und erstellen Sie eine App . Fertig ist der Platz für die Bewerbung. Wenn Sie auf „App öffnen“ klicken, leitet der Browser Sie zur Adresse Ihrer Anwendung weiter. Dies ist Ihre Webhook-Adresse – https://Ihr_Name.herokuapp.com/ . Registrieren Sie sie in Telegram und ändern Sie in den Einstellungen von application.propertie den Telegrambot. webHookPath=https://telegrambotsimpl.herokuapp.com/ zu Ihrem server.port=5000 kann gelöscht oder auskommentiert werden. Jetzt verbinden wir die Datenbank. Gehen Sie auf Heroku zur Registerkarte „RessourcenTelegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender!  Teil 2: - 1 “, klicken Sie auf „Hier finden Sie Heroku Postgres“ und klicken Sie auf „Installieren “. Sie werden auf die Seite Ihres Datenbankkontos weitergeleitet. Finden Sie es dort in den Einstellungen/ Telegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender!  Teil 2: - 2 Dort finden Sie alle notwendigen Daten aus Ihrer Datenbank. In application.properties sollte nun alles so aussehen:
#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
Ersetzen Sie die Daten aus Ihrem Konto durch Ihre: Im Feld jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org. postgresql.ssl .NonValidatingFactory muss durch die entsprechenden Daten aus dem Konto (Host, Datenbank) in Fettschrift ersetzt werden. Die Felder Benutzername und Passwort sind nicht schwer zu erraten. Jetzt müssen wir Tabellen in der Datenbank erstellen, ich habe das von IDEA aus gemacht. Unser Skript wird zum Erstellen einer Datenbank nützlich sein. Wir fügen die Datenbank wie oben beschrieben hinzu: Wir übernehmen Telegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender!  Teil 2: - 3 die Felder „Host“, „Benutzer“, „Passwort“ und „Datenbank“ aus dem Konto. Das URL- Feld ist bis zum Fragezeichen unser spring.datasource.url-Feld. Wir gehen zur Registerkarte „Erweitert“ , dort sollte es so aussehen: Telegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender!  Teil 2: - 4 Wenn Sie alles richtig gemacht haben, erscheint nach dem Klicken auf „Test“ ein grünes Häkchen. OK klicken. Klicken Sie mit der rechten Maustaste auf unsere Datenbank und wählen Sie Zur Abfragekonsole springen . Kopieren Sie unser Skript dorthin und klicken Sie auf Ausführen . Die Datenbank sollte erstellt werden. 10.000 Zeilen stehen Ihnen kostenlos zur Verfügung! Alles ist bereit für die Bereitstellung. Gehen Sie zu unserer Anwendung auf Heroku im Abschnitt „Bereitstellen“. Wählen Sie dort den Abschnitt Github aus: Telegram-Bot – Erinnerung per WebHook in Java oder Nein zum Google-Kalender!  Teil 2: - 5 Verknüpfen Sie Ihr Repository mit Heroku. Jetzt sind Ihre Zweige sichtbar. Vergessen Sie nicht, Ihre neuesten Änderungen an .properties zu übertragen. Wählen Sie unten den Zweig aus, der heruntergeladen werden soll, und klicken Sie auf Zweig bereitstellen . Wenn alles richtig gemacht wurde, werden Sie benachrichtigt, dass die Anwendung erfolgreich bereitgestellt wurde. Vergessen Sie nicht, „Automatische Bereitstellung von …“ zu aktivieren, damit Ihre Anwendung automatisch startet. Übrigens: Wenn Sie Änderungen an GitHub übertragen, startet Heroku die Anwendung automatisch neu. Seien Sie dabei vorsichtig, erstellen Sie einen separaten Thread für Mobbing und verwenden Sie den Hauptthread nur für die funktionierende Anwendung. Jetzt Billigkeit Nr. 2! Dies ist der bekannte Nachteil des kostenlosen Plans für Heroku. Wenn keine Nachrichten eingehen, geht die Anwendung in den Standby-Modus und nach dem Empfang einer Nachricht dauert der Start ziemlich lange, was nicht angenehm ist. Hierfür gibt es eine einfache Lösung – https://uptimerobot.com/ Und nein, Google-Ping-Gadgets helfen nicht weiter. Ich weiß nicht einmal, wo diese Informationen herkommen. Ich habe diese Frage gegoogelt, und seit etwa 10 Jahren funktioniert dieses Thema nicht mehr mit Sicherheit, wenn es überhaupt funktioniert. Diese Anwendung sendet HEAD-Anfragen für die von Ihnen festgelegte Zeit an die von Ihnen angegebene Adresse und sendet eine Nachricht per E-Mail, wenn sie nicht antwortet. Es wird Ihnen nicht schwer fallen, es herauszufinden, es gibt nicht genug Knöpfe, die Sie verwirren könnten)) Herzlichen Glückwunsch!! Wenn ich nichts vergessen habe und Sie aufmerksam waren, dann haben Sie Ihre eigene Anwendung, die kostenlos funktioniert und nie abstürzt. Die Gelegenheit für Mobbing und Experimente eröffnet sich vor Ihnen. Auf jeden Fall stehe ich gerne für Fragen zur Verfügung und nehme jede Kritik entgegen! Code: https://github.com/papoff8295/webHookBotForHabr Verwendete Materialien: https://tlgrm.ru/docs/bots/api – über Bots. https://en.wikibooks.org/wiki/Java_Persistence – über Beziehungen in Datenbanken. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-special-time-in-java – Zeitklasse und TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – wie man Code auf Github postet https://github.com/rubenlagus/TelegramBots – Telegram-Bibliothek und viele nützliche Informationen darüber.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION