diff --git a/autotests/calendarhandlerdata/flight.ics b/autotests/calendarhandlerdata/flight.ics index 0725b4b..4937cd4 100644 --- a/autotests/calendarhandlerdata/flight.ics +++ b/autotests/calendarhandlerdata/flight.ics @@ -1,25 +1,48 @@ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20171227T111649Z CREATED:20171227T111649Z -UID:XXX007-1b22236a-21ff-4885-8c99-b3b2bbca062c +UID:KIT-XXX007-1b22236a-21ff-4885-8c99-b3b2bbca062c LAST-MODIFIED:20171227T111649Z DESCRIPTION:Boarding time: 2:25 PM\nDeparture gate: 16\nBoarding group: C\nSeat: 16E\nBooking reference: XXX007 SUMMARY:Flight AB 8075 from HEL to TXL LOCATION:Helsinki GEO:60.317200;24.963301 DTSTART;TZID=Europe/Helsinki:20170920T150500 DTEND;TZID=Europe/Berlin:20170920T160000 TRANSP:OPAQUE +X-KDE-KITINERARY-RESERVATION:[{"@context":"http://schema.org"\,"@type": + "FlightReservation"\,"airplaneSeat":"16E"\,"boardingGroup": + "C"\,"modifyReservationUrl":"https: + //www.airberlin.com/de-DE/cockpit/index/index/bookingNo/XXX007/lastname/DO + E/submit/1"\,"reservationFor":{"@type":"Flight"\,"airline":{"@type": + "Airline"\,"iataCode":"AB"\,"name":"airberlin"}\,"arrivalAirport": + {"@type":"Airport"\,"geo":{"@type":"GeoCoordinates"\,"latitude": + 52.55970001220703\,"longitude":13.287799835205078}\,"iataCode": + "TXL"\,"name":"Berlin (Tegel)"}\,"arrivalTime":{"@type": + "QDateTime"\,"@value":"2017-09-20T16:00:00+02:00"\,"timezone": + "Europe/Berlin"}\,"boardingTime":{"@type":"QDateTime"\,"@value": + "2017-09-20T14:25:00+03:00"\,"timezone": + "Europe/Helsinki"}\,"departureAirport":{"@type":"Airport"\,"geo":{"@type": + "GeoCoordinates"\,"latitude":60.31719970703125\,"longitude": + 24.963300704956055}\,"iataCode":"HEL"\,"name":"Helsinki"}\,"departureDay": + "2017-09-20"\,"departureGate":"16"\,"departureTime":{"@type": + "QDateTime"\,"@value":"2017-09-20T15:05:00+03:00"\,"timezone": + "Europe/Helsinki"}\,"flightNumber":"8075"}\,"reservationNumber": + "XXX007"\,"reservedTicket":{"@type":"Ticket"\,"ticketToken":"https: + //checkin.airberlin.com/app/barcode.fly?pnr=XXX007&ln=DOE&cpid=12345678&fn + =JOHN"}\,"ticketDownloadUrl":"https: + //m.airberlin.com/ckbc/pnr/XXX007/ln/DOE/cpid/94482631/fn/JOHN"\,"underNam + e":{"@type":"Person"\,"name":"JOHN DOE"}}] BEGIN:VALARM DESCRIPTION:Boarding for flight AB 8075 at gate 16 ACTION:DISPLAY TRIGGER;VALUE=DURATION:-PT40M X-KDE-KCALCORE-ENABLED:TRUE END:VALARM END:VEVENT END:VCALENDAR diff --git a/autotests/calendarhandlerdata/hotel.ics b/autotests/calendarhandlerdata/hotel.ics index 9f0f983..2fb4567 100644 --- a/autotests/calendarhandlerdata/hotel.ics +++ b/autotests/calendarhandlerdata/hotel.ics @@ -1,17 +1,32 @@ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20171227T111649Z CREATED:20171227T111649Z -UID:1234567890-1b22236a-21ff-4885-8c99-b3b2bbca062c +UID:KIT-1234567890-1b22236a-21ff-4885-8c99-b3b2bbca062c LAST-MODIFIED:20171227T111649Z DESCRIPTION:Check-in: 3:00 PM\nCheck-out: 12:00 PM\nBooking reference: 1234567890 SUMMARY:Hotel reservation: Glo Hotel Sello LOCATION:Leppävaarankatu 1\, 02600 Espoo\, Finland DTSTART;VALUE=DATE:20170919 DTEND;VALUE=DATE:20170921 TRANSP:TRANSPARENT +X-KDE-KITINERARY-RESERVATION:[{"@context":"http://schema.org"\,"@type": + "LodgingReservation"\,"cancelReservationUrl":"https: + //secure.booking.com/mybooking.en-gb.html?auth_key=magic&source=conf_metad + ata&pbsource=email_cancel"\,"checkinTime":"2017-09-19T15:00:00+03: + 00"\,"checkoutTime":"2017-09-20T12:00:00+03:00"\,"reservationFor": + {"@type":"LodgingBusiness"\,"address":{"@type": + "PostalAddress"\,"addressCountry":"Finland"\,"addressLocality": + "Espoo"\,"addressRegion":""\,"postalCode":"02600"\,"streetAddress": + "Leppävaarankatu 1"}\,"name":"Glo Hotel Sello"\,"telephone": + "+358101234567"\,"url":"https: + //www.booking.com/hotel/fi/palace-sello.html?aid=123456&label=postbooking_ + confemail"}\,"reservationNumber":"1234567890"\,"underName":{"@type": + "Person"\,"email":"john.doe@email.com"\,"name":"John Doe"}\,"url":"https: + //secure.booking.com/mybooking.en-gb.html?aid=123456\; + auth_key=magic&&source=conf_metadata&pbsource=conf_email_modify"}] END:VEVENT END:VCALENDAR diff --git a/autotests/calendarhandlerdata/train.ics b/autotests/calendarhandlerdata/train.ics index 6c4a6d3..12ba1bd 100644 --- a/autotests/calendarhandlerdata/train.ics +++ b/autotests/calendarhandlerdata/train.ics @@ -1,18 +1,31 @@ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20171227T111649Z CREATED:20171227T111649Z -UID:XXX007-1b22236a-21ff-4885-8c99-b3b2bbca062c +UID:KIT-XXX007-1b22236a-21ff-4885-8c99-b3b2bbca062c LAST-MODIFIED:20171227T111649Z DESCRIPTION:Coach: 17\nSeat: 62\nBooking reference: XXX007 SUMMARY:Train 5186 from Nîmes Gare to Lyon Part-Dieu LOCATION:Nîmes Gare GEO:43.832291;4.365845 DTSTART;TZID="UTC+02:00":20170929T182600 DTEND;TZID="UTC+02:00":20170929T195200 TRANSP:OPAQUE +X-KDE-KITINERARY-RESERVATION:[{"@context":"http://schema.org"\,"@type": + "TrainReservation"\,"reservationFor":{"@type": + "TrainTrip"\,"arrivalStation":{"@type":"TrainStation"\,"geo":{"@type": + "GeoCoordinates"\,"latitude":45.76055908203125\,"longitude": + 4.8593549728393555}\,"name":"Lyon Part-Dieu"}\,"arrivalTime": + "2017-09-29T19:52:00+02:00"\,"departureStation":{"@type": + "TrainStation"\,"geo":{"@type":"GeoCoordinates"\,"latitude": + 43.83229064941406\,"longitude":4.365845203399658}\,"name":"Nîmes Gare"}\,"departureTime":"2017-09-29T18:26:00+02:00"\,"provider":{"@type": + "Organization"\,"name":"SNCF"}\,"trainName":"TGV"\,"trainNumber": + "5186"}\,"reservationNumber":"XXX007"\,"reservedTicket":{"@type": + "Ticket"\,"ticketToken":"aztecCode:somerandomdata DOE JOHN111110 00000"\,"ticketedSeat":{"@type": + "Seat"\,"seatNumber":"62"\,"seatSection":"17"}}\,"underName":{"@type": + "Person"\,"name":"John Doe"}\,"url":"https://www.trainline.fr/tickets"}] END:VEVENT END:VCALENDAR diff --git a/src/calendarhandler.cpp b/src/calendarhandler.cpp index 79a5f19..18b7271 100644 --- a/src/calendarhandler.cpp +++ b/src/calendarhandler.cpp @@ -1,244 +1,273 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "calendarhandler.h" #include "jsonlddocument.h" #include "logging.h" +#include "mergeutil.h" #include #include #include #include #include #include #include #include +#include +#include + using namespace KCalCore; using namespace KItinerary; static void fillFlightReservation(const FlightReservation &reservation, const KCalCore::Event::Ptr &event); static void fillTripReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event); static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event); static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event); static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event); static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event); QDateTime CalendarHandler::startDateTime(const QVariant &reservation) { - if (reservation.userType() == qMetaTypeId() - || reservation.userType() == qMetaTypeId() + if (reservation.userType() == qMetaTypeId()) { + const auto flight = reservation.value().reservationFor().value(); + if (flight.departureTime().isValid()) { + return flight.departureTime(); + } + return QDateTime(flight.departureDay(), QTime()); + } else if (reservation.userType() == qMetaTypeId() || reservation.userType() == qMetaTypeId()) { const auto trip = JsonLdDocument::readProperty(reservation, "reservationFor"); return JsonLdDocument::readProperty(trip, "departureTime").toDateTime(); } else if (reservation.userType() == qMetaTypeId()) { return reservation.value().checkinTime(); } return {}; } Event::Ptr CalendarHandler::findEvent(const Calendar::Ptr &calendar, const QVariant &reservation) { if (!JsonLd::canConvert(reservation)) { return {}; } - const auto bookingRef = JsonLd::convert(reservation).reservationNumber(); + auto bookingRef = JsonLd::convert(reservation).reservationNumber(); if (bookingRef.isEmpty()) { return {}; } + bookingRef.prepend(QLatin1String("KIT-")); auto dt = startDateTime(reservation); if (reservation.userType() == qMetaTypeId()) { dt = QDateTime(dt.date(), QTime()); } - const auto events = calendar->events(dt); + const auto events = calendar->events(dt.date()); for (const auto &event : events) { - if (event->dtStart() == dt && event->uid().startsWith(bookingRef)) { + if (!event->uid().startsWith(bookingRef)) { + continue; + } + const auto otherRes = CalendarHandler::reservationForEvent(event); + if (MergeUtil::isSameReservation(otherRes, reservation)) { return event; } } + return {}; } +QVariant CalendarHandler::reservationForEvent(const KCalCore::Event::Ptr &event) +{ + const auto payload = event->customProperty("KITINERARY", "RESERVATION").toUtf8(); + const auto json = QJsonDocument::fromJson(payload).array(); + const auto data = JsonLdDocument::fromJson(json); + if (data.size() != 1) { + return {}; + } + return data.at(0); +} + void CalendarHandler::fillEvent(const QVariant &reservation, const KCalCore::Event::Ptr &event) { const int typeId = reservation.userType(); if (typeId == qMetaTypeId()) { fillFlightReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillLodgingReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillTrainReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillBusReservation(reservation.value(), event); } else { return; } const auto bookingRef = JsonLd::convert(reservation).reservationNumber(); - if (!event->uid().startsWith(bookingRef)) { - event->setUid(bookingRef + QLatin1Char('-') + event->uid()); + if (!event->uid().startsWith(QLatin1String("KIT-") + bookingRef)) { + event->setUid(QLatin1String("KIT-") + bookingRef + QLatin1Char('-') + event->uid()); } + + const auto payload = QJsonDocument(JsonLdDocument::toJson({reservation})).toJson(QJsonDocument::Compact); + event->setCustomProperty("KITINERARY", "RESERVATION", QString::fromUtf8(payload)); } static void fillFlightReservation(const FlightReservation &reservation, const KCalCore::Event::Ptr &event) { const auto flight = reservation.reservationFor().value(); const auto airline = flight.airline(); const auto depPort = flight.departureAirport(); const auto arrPort = flight.arrivalAirport(); const QString flightNumber = airline.iataCode() + QLatin1Char(' ') + flight.flightNumber(); event->setSummary(i18n("Flight %1 from %2 to %3", flightNumber, depPort.iataCode(), arrPort.iataCode())); event->setLocation(depPort.name()); fillGeoPosition(depPort, event); event->setDtStart(flight.departureTime()); event->setDtEnd(flight.arrivalTime()); event->setAllDay(false); const auto boardingTime = flight.boardingTime(); const auto departureGate = flight.departureGate(); if (boardingTime.isValid()) { Alarm::Ptr alarm(new Alarm(event.data())); alarm->setStartOffset(Duration(event->dtStart(), boardingTime)); if (departureGate.isEmpty()) { alarm->setDisplayAlarm(i18n("Boarding for flight %1", flightNumber)); } else { alarm->setDisplayAlarm(i18n("Boarding for flight %1 at gate %2", flightNumber, departureGate)); } alarm->setEnabled(true); event->addAlarm(alarm); } QStringList desc; if (boardingTime.isValid()) { desc.push_back(i18n("Boarding time: %1", QLocale().toString(boardingTime.time(), QLocale::ShortFormat))); } if (!departureGate.isEmpty()) { desc.push_back(i18n("Departure gate: %1", departureGate)); } if (!reservation.boardingGroup().isEmpty()) { desc.push_back(i18n("Boarding group: %1", reservation.boardingGroup())); } if (!reservation.airplaneSeat().isEmpty()) { desc.push_back(i18n("Seat: %1", reservation.airplaneSeat())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillTripReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event) { const auto trip = JsonLdDocument::readProperty(reservation, "reservationFor"); const auto depStation = JsonLdDocument::readProperty(trip, "departureStation"); const auto arrStation = JsonLdDocument::readProperty(trip, "arrivalStation"); event->setLocation(JsonLdDocument::readProperty(depStation, "name").toString()); fillGeoPosition(depStation, event); event->setDtStart(JsonLdDocument::readProperty(trip, "departureTime").toDateTime()); event->setDtEnd(JsonLdDocument::readProperty(trip, "arrivalTime").toDateTime()); event->setAllDay(false); QStringList desc; auto s = JsonLdDocument::readProperty(trip, "departurePlatform").toString(); if (!s.isEmpty()) { desc.push_back(i18n("Departure platform: %1", s)); } const auto ticket = JsonLdDocument::readProperty(reservation, "reservedTicket"); const auto seat = JsonLdDocument::readProperty(ticket, "ticketedSeat"); s = JsonLdDocument::readProperty(seat, "seatSection").toString(); if (!s.isEmpty()) { desc.push_back(i18n("Coach: %1", s)); } s = JsonLdDocument::readProperty(seat, "seatNumber").toString(); if (!s.isEmpty()) { desc.push_back(i18n("Seat: %1", s)); } s = JsonLdDocument::readProperty(trip, "arrivalPlatform").toString(); if (!s.isEmpty()) { desc.push_back(i18n("Arrival platform: %1", s)); } s = JsonLdDocument::readProperty(reservation, "reservationNumber").toString(); if (!s.isEmpty()) { desc.push_back(i18n("Booking reference: %1", s)); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); const auto depStation = trip.departureStation(); const auto arrStation = trip.arrivalStation(); event->setSummary(i18n("Train %1 from %2 to %3", trip.trainNumber(), depStation.name(), arrStation.name())); fillTripReservation(reservation, event); } static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); const auto depStation = trip.departureStation(); const auto arrStation = trip.arrivalStation(); event->setSummary(i18n("Bus %1 from %2 to %3", trip.busNumber(), depStation.name(), arrStation.name())); fillTripReservation(reservation, event); } static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event) { const auto lodgingBusiness = reservation.reservationFor().value(); const auto address = lodgingBusiness.address(); event->setSummary(i18n("Hotel reservation: %1", lodgingBusiness.name())); event->setLocation(i18nc(", , ", "%1, %2 %3, %4", address.streetAddress(), address.postalCode(), address.addressLocality(), address.addressCountry())); fillGeoPosition(lodgingBusiness, event); event->setDtStart(QDateTime(reservation.checkinTime().date(), QTime())); event->setDtEnd(QDateTime(reservation.checkoutTime().date(), QTime())); event->setAllDay(true); event->setDescription(i18n("Check-in: %1\nCheck-out: %2\nBooking reference: %3", QLocale().toString(reservation.checkinTime().time(), QLocale::ShortFormat), QLocale().toString(reservation.checkoutTime().time(), QLocale::ShortFormat), reservation.reservationNumber())); event->setTransparency(Event::Transparent); } static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event) { if (!JsonLd::canConvert(place)) { return; } const auto geo = JsonLd::convert(place).geo(); if (!geo.isValid()) { return; } event->setHasGeo(true); event->setGeoLatitude(geo.latitude()); event->setGeoLongitude(geo.longitude()); } diff --git a/src/calendarhandler.h b/src/calendarhandler.h index 140adf9..dbc26c7 100644 --- a/src/calendarhandler.h +++ b/src/calendarhandler.h @@ -1,49 +1,52 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CALENDARHANDLER_H #define CALENDARHANDLER_H #include "kitinerary_export.h" #include #include class QVariant; namespace KItinerary { /** Methods for converting between ical events and JSON-LD booking data. */ namespace CalendarHandler { /** Returns the start time associated with the given reservation. */ KITINERARY_EXPORT QDateTime startDateTime(const QVariant &reservation); /** Attempts to find an event in @p calendar for @p reservation. */ KITINERARY_EXPORT KCalCore::Event::Ptr findEvent(const KCalCore::Calendar::Ptr &calendar, const QVariant &reservation); + /** Returns the reservation object for this event. */ + KITINERARY_EXPORT QVariant reservationForEvent(const KCalCore::Event::Ptr &event); + /** Fills @p event with details of @p reservation. * Can be used on new events or to update existing ones. */ KITINERARY_EXPORT void fillEvent(const QVariant &reservation, const KCalCore::Event::Ptr &event); } } #endif // CALENDARHANDLER_H