diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.cpp b/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.cpp index 4ad5cd4f..7244e184 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.cpp +++ b/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.cpp @@ -1,149 +1,154 @@ /* 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 "itinerarymemento.h" #include #include #include #include #include #include #include using namespace KItinerary; void ItineraryMemento::detach() { } +const char* ItineraryMemento::identifier() +{ + return "org.kde.messageviewer.itineraryData"; +} + bool ItineraryMemento::isParsed(const KMime::ContentIndex &index) const { return m_parsedParts.contains(index); } void ItineraryMemento::setParsed(const KMime::ContentIndex &index) { m_parsedParts.insert(index); } void ItineraryMemento::setMessageDate(const QDateTime &contextDt) { m_postProc.setContextDate(contextDt); } void ItineraryMemento::appendData(const QVector &data) { m_postProc.process(data); } bool ItineraryMemento::hasData() const { return !m_data.isEmpty() || !m_postProc.result().isEmpty(); } QVector ItineraryMemento::data() { if (m_data.isEmpty() && !m_postProc.result().isEmpty()) { // perform calendar lookup and merge results std::vector > resolvedEvents; resolvedEvents.reserve(m_postProc.result().size()); const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &r : m_postProc.result()) { auto e = std::make_pair(r, CalendarHandler::findEvent(calendar, r)); if (e.second) { const auto existingRes = CalendarHandler::reservationsForEvent(e.second); for (const auto &ev : existingRes) { if (MergeUtil::isSame(ev, e.first)) { e.first = MergeUtil::merge(ev, e.first); break; } } } resolvedEvents.push_back(e); } // merge multi-traveler elements for (auto it = resolvedEvents.begin(); it != resolvedEvents.end();) { TripData data; data.reservations.push_back((*it).first); data.event = (*it).second; data.expanded = false; ++it; if (JsonLd::canConvert(data.reservations.at(0))) { const auto trip = JsonLd::convert(data.reservations.at(0)).reservationFor(); for (; it != resolvedEvents.end(); ++it) { if (!JsonLd::canConvert((*it).first) || !MergeUtil::isSame(JsonLd::convert((*it).first).reservationFor(), trip)) { break; } data.reservations.push_back((*it).first); } // add other traveler we already know about from previous data in the calendar // but that aren't in the currently extracted data set if (data.event) { for (const auto &prev : CalendarHandler::reservationsForEvent(data.event)) { const auto notFound = std::find_if(data.reservations.constBegin(), data.reservations.constEnd(), [prev](const QVariant &v) { return MergeUtil::isSame(v, prev); }) == data.reservations.constEnd(); if (notFound) { data.reservations.push_back(prev); } } } } m_data.push_back(data); } } return m_data; } void ItineraryMemento::toggleExpanded(int index) { if (index >= m_data.size()) { return; } m_data[index].expanded = !m_data.at(index).expanded; } void ItineraryMemento::addPass(KPkPass::Pass *pass, const QByteArray &rawData) { m_passes.emplace_back(PassData{pass->passTypeIdentifier(), pass->serialNumber(), rawData}); } QByteArray ItineraryMemento::rawPassData(const QString &passTypeIdentifier, const QString &serialNumber) const { for (const auto &pass : m_passes) { if (pass.passTypeIdentifier == passTypeIdentifier && pass.serialNumber == serialNumber) { return pass.rawData; } } return {}; } void ItineraryMemento::addDocument(const QString &docId, const QVariant &docInfo, const QByteArray &docData) { m_docs.push_back({docId, docInfo, docData}); } diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.h b/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.h index 6d5b007f..89d451a9 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.h +++ b/plugins/messageviewer/bodypartformatter/itinerary/itinerarymemento.h @@ -1,92 +1,94 @@ /* 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 ITINERARYMEMENTO_H #define ITINERARYMEMENTO_H #include #include #include #include #include #include #include namespace KPkPass { class Pass; } namespace KMime { class ContentIndex; } class QDateTime; /** Memento holding the itinerary information extracted for an email. */ class ItineraryMemento : public MimeTreeParser::Interface::BodyPartMemento { public: ~ItineraryMemento() override = default; void detach() override; + static const char* identifier(); + bool isParsed(const KMime::ContentIndex &index) const; void setParsed(const KMime::ContentIndex &index); void setMessageDate(const QDateTime &contextDt); void appendData(const QVector &data); bool hasData() const; struct TripData { QVector reservations; KCalendarCore::Event::Ptr event; bool expanded; }; QVector data(); void toggleExpanded(int index); void addPass(KPkPass::Pass *pass, const QByteArray &rawData); QByteArray rawPassData(const QString &passTypeIdentifier, const QString &serialNumber) const; struct PassData { QString passTypeIdentifier; QString serialNumber; QByteArray rawData; }; const std::vector& passData() const { return m_passes; } struct DocumentData { QString docId; QVariant docInfo; QByteArray rawData; }; const std::vector& documentData() const { return m_docs; } void addDocument(const QString &docId, const QVariant &docInfo, const QByteArray &docData); private: QSet m_parsedParts; KItinerary::ExtractorPostprocessor m_postProc; QVector m_data; std::vector m_passes; std::vector m_docs; }; #endif // ITINERARYMEMENTO_H diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itineraryprocessor.cpp b/plugins/messageviewer/bodypartformatter/itinerary/itineraryprocessor.cpp index 78662ff9..8986ab1f 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itineraryprocessor.cpp +++ b/plugins/messageviewer/bodypartformatter/itinerary/itineraryprocessor.cpp @@ -1,154 +1,154 @@ /* 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 "itineraryprocessor.h" #include "itinerarymemento.h" #include "itinerary_debug.h" #include #include #include #include #include #include #include using namespace KItinerary; static bool isPkPassContent(KMime::Content *content) { const auto ct = content->contentType(); if (ct->mimeType() == "application/vnd.apple.pkpass") { return true; } if (ct->mimeType() != "application/octet-stream" && ct->mimeType() != "application/zip") { return false; } if (ct->name().endsWith(QLatin1String("pkpass"))) { return true; } const auto cd = content->contentDisposition(false); return cd && cd->filename().endsWith(QLatin1String("pkpass")); } static bool isCalendarContent(KMime::Content *content) { const auto ct = content->contentType(); if (ct->mimeType() == "text/calendar") { return true; } if (ct->mimeType() != "text/plain" && ct->mimeType() != "application/octet-stream") { return false; } if (ct->name().endsWith(QLatin1String(".ics"))) { return true; } const auto cd = content->contentDisposition(false); return cd && cd->filename().endsWith(QLatin1String(".ics")); } ItineraryProcessor::ItineraryProcessor() = default; ItineraryProcessor::~ItineraryProcessor() = default; MimeTreeParser::MessagePart::Ptr ItineraryProcessor::process(MimeTreeParser::Interface::BodyPart &part) const { auto nodeHelper = part.nodeHelper(); if (!nodeHelper) { return {}; } // determine sender date of the current part (differs from topLevel()->date() for forwarded mails QDateTime senderDateTime; auto node = part.content(); auto dateHdr = node->header(); while (!dateHdr && node->parent()) { node = node->parent(); dateHdr = node->header(); } if (dateHdr) { senderDateTime = dateHdr->dateTime(); } - auto memento = dynamic_cast(nodeHelper->bodyPartMemento(part.topLevelContent(), "org.kde.messageviewer.semanticData")); + auto memento = dynamic_cast(nodeHelper->bodyPartMemento(part.topLevelContent(), ItineraryMemento::identifier())); if (!memento) { memento = new ItineraryMemento; memento->setMessageDate(senderDateTime); - nodeHelper->setBodyPartMemento(part.topLevelContent(), "org.kde.messageviewer.semanticData", memento); + nodeHelper->setBodyPartMemento(part.topLevelContent(), ItineraryMemento::identifier(), memento); } // check if we still have to do anything at all if (memento->isParsed(part.content()->index())) { return {}; } memento->setParsed(part.content()->index()); std::vector extractors; std::unique_ptr pass; bool isPdf = false; ExtractorEngine engine; engine.setUseSeparateProcess(true); engine.setContext(part.content()); if (isPkPassContent(part.content())) { pass.reset(KPkPass::Pass::fromData(part.content()->decodedContent())); engine.setPass(pass.get()); } else if (part.content()->contentType()->isHTMLText()) { engine.setData(part.content()->decodedContent(), ExtractorInput::Html); } else if (part.content()->contentType()->mimeType() == "application/pdf") { isPdf = true; engine.setData(part.content()->decodedContent(), ExtractorInput::Pdf); } else if (isCalendarContent(part.content())) { engine.setData(part.content()->decodedContent(), ExtractorInput::ICal); } else if (part.content()->contentType()->isPlainText()) { engine.setText(part.content()->decodedText()); } else { // we have extractors but this isn't a mimetype we understand return {}; } const auto data = engine.extract(); //qCDebug(ITINERARY_LOG).noquote() << QJsonDocument(data).toJson(); auto decodedData = JsonLdDocument::fromJson(data); if (!decodedData.isEmpty()) { if (isPdf) { const auto docData = part.content()->decodedContent(); const auto docId = DocumentUtil::idForContent(docData); DigitalDocument docInfo; docInfo.setEncodingFormat(QLatin1String("application/pdf")); docInfo.setName(MimeTreeParser::NodeHelper::fileName(part.content())); memento->addDocument(docId, docInfo, docData); for (auto &res : decodedData) { DocumentUtil::addDocumentId(res, docId); } } memento->appendData(decodedData); } if (pass) { memento->addPass(pass.get(), part.content()->decodedContent()); } qCDebug(ITINERARY_LOG) << "-------------------------------------------- END ITINERARY PARSING"; return {}; } diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itineraryrenderer.cpp b/plugins/messageviewer/bodypartformatter/itinerary/itineraryrenderer.cpp index 6dc9eda0..31eb2c0c 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itineraryrenderer.cpp +++ b/plugins/messageviewer/bodypartformatter/itinerary/itineraryrenderer.cpp @@ -1,215 +1,215 @@ /* 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 "itineraryrenderer.h" #include "itinerarymemento.h" #include "itinerary_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; // Grantlee has no Q_GADGET support yet #define GRANTLEE_MAKE_GADGET(Class) \ GRANTLEE_BEGIN_LOOKUP(Class) \ const auto idx = Class::staticMetaObject.indexOfProperty(property.toUtf8().constData()); \ if (idx < 0) { \ return {};} \ const auto mp = Class::staticMetaObject.property(idx); \ return mp.readOnGadget(&object); \ GRANTLEE_END_LOOKUP GRANTLEE_MAKE_GADGET(Airport) GRANTLEE_MAKE_GADGET(Airline) GRANTLEE_MAKE_GADGET(Event) GRANTLEE_MAKE_GADGET(EventReservation) GRANTLEE_MAKE_GADGET(Flight) GRANTLEE_MAKE_GADGET(FlightReservation) GRANTLEE_MAKE_GADGET(LodgingBusiness) GRANTLEE_MAKE_GADGET(LodgingReservation) GRANTLEE_MAKE_GADGET(Person) GRANTLEE_MAKE_GADGET(Place) GRANTLEE_MAKE_GADGET(PostalAddress) GRANTLEE_MAKE_GADGET(Seat) GRANTLEE_MAKE_GADGET(Ticket) GRANTLEE_MAKE_GADGET(TrainStation) GRANTLEE_MAKE_GADGET(TrainTrip) GRANTLEE_MAKE_GADGET(TrainReservation) GRANTLEE_MAKE_GADGET(BusStation) GRANTLEE_MAKE_GADGET(BusTrip) GRANTLEE_MAKE_GADGET(BusReservation) GRANTLEE_MAKE_GADGET(CancelAction) GRANTLEE_MAKE_GADGET(CheckInAction) GRANTLEE_MAKE_GADGET(DownloadAction) GRANTLEE_MAKE_GADGET(UpdateAction) GRANTLEE_MAKE_GADGET(ViewAction) GRANTLEE_MAKE_GADGET(FoodEstablishment) GRANTLEE_MAKE_GADGET(FoodEstablishmentReservation) GRANTLEE_MAKE_GADGET(RentalCarReservation) GRANTLEE_MAKE_GADGET(RentalCar) GRANTLEE_MAKE_GADGET(Brand) GRANTLEE_MAKE_GADGET(Organization) ItineraryRenderer::ItineraryRenderer() { Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); Grantlee::registerMetaType(); } bool ItineraryRenderer::render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *htmlWriter, MessageViewer::RenderContext *context) const { Q_UNUSED(context); const auto mpList = msgPart.dynamicCast(); if (!msgPart->isRoot() || !mpList->hasSubParts()) { return false; } const auto node = mpList->subParts().at(0)->content(); const auto nodeHelper = msgPart->nodeHelper(); if (!nodeHelper || !node) { return false; } - auto memento = dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), "org.kde.messageviewer.semanticData")); + auto memento = dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), ItineraryMemento::identifier())); if (!memento || !memento->hasData()) { return false; } const auto extractedData = memento->data(); if (extractedData.isEmpty()) { // hasData() will not be correct for filtered structured data on the first pass through here... return false; } const auto dir = nodeHelper->createTempDir(QStringLiteral("semantic")); auto c = MessageViewer::MessagePartRendererManager::self()->createContext(); const auto pal = qGuiApp->palette(); QVariantMap style; style.insert(QStringLiteral("frameColor"), pal.link().color().name()); style.insert(QStringLiteral("expandIcon"), QString(QStringLiteral("file://") + MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quoteexpand.png")))); style.insert(QStringLiteral("collapseIcon"), QString(QStringLiteral("file://") + MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quotecollapse.png")))); c.insert(QStringLiteral("style"), style); // Grantlee can't do indexed map/array lookups, so we need to interleave this here already QVariantList elems; elems.reserve(extractedData.size()); for (int i = 0; i < extractedData.size(); ++i) { QVariantMap data; QVariantMap state; const auto d = extractedData.at(i); state.insert(QStringLiteral("expanded"), d.expanded); data.insert(QStringLiteral("state"), state); data.insert(QStringLiteral("groupId"), i); QVector reservations; for (const auto &r : d.reservations) { QVariantMap m; m.insert(QStringLiteral("reservation"), r); // generate ticket barcodes const auto ticket = JsonLd::convert(r).reservedTicket().value(); std::unique_ptr barcode; switch (ticket.ticketTokenType()) { case Ticket::AztecCode: barcode.reset(Prison::createBarcode(Prison::Aztec)); barcode->setData(ticket.ticketTokenData()); break; case Ticket::QRCode: barcode.reset(Prison::createBarcode(Prison::QRCode)); barcode->setData(ticket.ticketTokenData()); break; default: break; } if (barcode) { barcode->toImage(barcode->minimumSize()); // minimumSize is only available after we rendered once... const auto img = barcode->toImage(barcode->minimumSize()); const QString fileName = dir + QStringLiteral("/ticketToken") + QString::number(i) + QStringLiteral(".png"); img.save(fileName); m.insert(QStringLiteral("ticketToken"), fileName); nodeHelper->addTempFile(fileName); } reservations.push_back(m); } data.insert(QStringLiteral("reservations"), QVariant::fromValue(reservations)); elems.push_back(data); } c.insert(QStringLiteral("data"), elems); auto t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral("org.kde.messageviewer/itinerary/itinerary.html")); const_cast(t->engine())->addDefaultLibrary(QStringLiteral("kitinerary_grantlee_extension")); Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); return false; // yes, false, we want the rest of the email rendered normally after this } diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp b/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp index 37a25ce6..bef0e283 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp +++ b/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp @@ -1,418 +1,418 @@ /* 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 "itineraryurlhandler.h" #include "itinerarymemento.h" #include "itinerary_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; ItineraryUrlHandler::ItineraryUrlHandler() { m_appPath = QStandardPaths::findExecutable(QStringLiteral("itinerary")); } QString ItineraryUrlHandler::name() const { return QString::fromUtf8(staticMetaObject.className()); } static bool canAddToCalendar(ItineraryMemento *m) { for (const auto &d : m->data()) { if (JsonLd::isA(d.reservations.at(0))) { const auto f = d.reservations.at(0).value().reservationFor().value(); if (f.departureTime().isValid() && f.arrivalTime().isValid()) { return true; } continue; } else if (SortUtil::startDateTime(d.reservations.at(0)).isValid()) { return true; } } return false; } bool ItineraryUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(viewerInstance); if (path == QLatin1String("semanticAction")) { const auto m = memento(part); if (!m || !m->hasData()) { qCWarning(ITINERARY_LOG) << "sementic action: data not found"; return true; } handleContextMenuRequest(part, path, QCursor::pos()); return true; } if (path.startsWith(QLatin1String("semanticExpand?"))) { auto idx = path.midRef(15).toInt(); auto m = memento(part); m->toggleExpanded(idx); const auto nodeHelper = part->nodeHelper(); emit nodeHelper->update(MimeTreeParser::Delayed); return true; } return false; } static QString escapePlaceName(const QString &name) { return QString(name).replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; } static void addGoToMapAction(QMenu *menu, const GeoCoordinates &geo, const QString &placeName, int zoom = 17) { if (geo.isValid()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); QObject::connect(action, &QAction::triggered, menu, [geo, zoom]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/")); const QString fragment = QLatin1String("map=") + QString::number(zoom) + QLatin1Char('/') + QString::number(geo.latitude()) + QLatin1Char('/') + QString::number(geo.longitude()); url.setFragment(fragment); QDesktopServices::openUrl(url); }); } } static void addGoToMapAction(QMenu *menu, const PostalAddress &addr, const QString &placeName) { if (!addr.addressLocality().isEmpty()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); QObject::connect(action, &QAction::triggered, menu, [addr]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/search")); const QString queryString = addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); QUrlQuery query; query.addQueryItem(QStringLiteral("query"), queryString); url.setQuery(query); QDesktopServices::openUrl(url); }); } } static void addGoToMapAction(QMenu *menu, const QVariant &place, QSet &places) { const auto name = LocationUtil::name(place); if (places.contains(name)) { return; } places.insert(name); const auto geo = LocationUtil::geo(place); const auto zoom = JsonLd::isA(place) ? 12 : 17; if (geo.isValid()) { addGoToMapAction(menu, geo, name, zoom); } else { addGoToMapAction(menu, LocationUtil::address(place), name); } } bool ItineraryUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const { Q_UNUSED(part); if (path != QLatin1String("semanticAction")) { return false; } const auto m = memento(part); if (!m || !m->hasData()) { return false; } const auto date = dateForReservation(m); QMenu menu; QAction *action = nullptr; if (date.isValid()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("view-calendar")), i18n("Show Calendar")); QObject::connect(action, &QAction::triggered, this, [this, date](){ showCalendar(date); }); } action = menu.addAction(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("Add To Calendar")); action->setEnabled(canAddToCalendar(m)); QObject::connect(action, &QAction::triggered, this, [this, m](){ addToCalendar(m); }); QSet places; for (const auto &d : m->data()) { const auto res = d.reservations.at(0); // for multi-traveler reservations all subsequent ones are equal regarding what we are interested here if (LocationUtil::isLocationChange(res)) { const auto dep = LocationUtil::departureLocation(res); addGoToMapAction(&menu, dep, places); const auto arr = LocationUtil::arrivalLocation(res); addGoToMapAction(&menu, arr, places); } else { const auto loc = LocationUtil::location(res); addGoToMapAction(&menu, loc, places); } } if (!m_appPath.isEmpty()) { menu.addSeparator(); action = menu.addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Import into KDE Itinerary")); QObject::connect(action, &QAction::triggered, this, [this, part]() { openInApp(part); }); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral( "devices")); msg.setArguments({true, true}); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); reply.waitForFinished(); if (reply.isValid()) { for (const QString &deviceId : reply.value()) { QDBusInterface deviceIface(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId, QStringLiteral("org.kde.kdeconnect.device")); QDBusReply pluginReply = deviceIface.call(QStringLiteral("hasPlugin"), QLatin1String("kdeconnect_share")); if (pluginReply.value()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send to %1", deviceIface.property("name").toString())); QObject::connect(action, &QAction::triggered, this, [this, part, deviceId]() { openWithKDEConnect(part, deviceId); }); } } } menu.exec(p); return true; } QString ItineraryUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(part); if (path == QLatin1String("semanticAction")) { return i18n("Add reservation to your calendar."); } return {}; } ItineraryMemento *ItineraryUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const { const auto node = part->content()->topLevel(); const auto nodeHelper = part->nodeHelper(); if (!nodeHelper || !node) { return nullptr; } - return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), "org.kde.messageviewer.semanticData")); + return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), ItineraryMemento::identifier())); } QDate ItineraryUrlHandler::dateForReservation(ItineraryMemento *memento) const { for (const auto &d : memento->data()) { const auto dt = SortUtil::startDateTime(d.reservations.at(0)); if (dt.isValid()) { return dt.date(); } } return {}; } void ItineraryUrlHandler::showCalendar(const QDate &date) const { // ensure KOrganizer or Kontact are running if (KontactInterface::PimUniqueApplication::activateApplication(QLatin1String("korganizer"))) { // select the date of the reservation QDBusInterface korgIface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QStringLiteral("org.kde.Korganizer.Calendar"), QDBusConnection::sessionBus()); if (!korgIface.isValid()) { qCWarning(ITINERARY_LOG) << "Calendar interface is not valid! " << korgIface.lastError().message(); return; } korgIface.call(QStringLiteral("showEventView")); korgIface.call(QStringLiteral("showDate"), date); } } static void attachPass(const KCalendarCore::Event::Ptr &event, const QVector &reservations, ItineraryMemento *memento) { for (const auto &reservation : reservations) { if (!JsonLd::canConvert(reservation)) { return; } const auto res = JsonLd::convert(reservation); const auto data = memento->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (data.isEmpty()) { return; } event->deleteAttachments(QStringLiteral("application/vnd.apple.pkpass")); using namespace KCalendarCore; Attachment att(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass")); att.setLabel(i18n("Boarding Pass")); // TODO add passenger name after string freeze is lifted event->addAttachment(att); } } void ItineraryUrlHandler::addToCalendar(ItineraryMemento *memento) const { using namespace KCalendarCore; const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &d : memento->data()) { auto event = d.event; if (!event) { event.reset(new KCalendarCore::Event); CalendarHandler::fillEvent(d.reservations, event); if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) { continue; } attachPass(event, d.reservations, memento); calendar->addEvent(event); } else { event->startUpdates(); CalendarHandler::fillEvent(d.reservations, event); event->endUpdates(); attachPass(event, d.reservations, memento); calendar->modifyIncidence(event); } } } void ItineraryUrlHandler::openInApp(MimeTreeParser::Interface::BodyPart *part) const { const auto fileName = createItineraryFile(part); QProcess::startDetached(m_appPath, {fileName}); } void ItineraryUrlHandler::openWithKDEConnect(MimeTreeParser::Interface::BodyPart *part, const QString &deviceId) const { const auto fileName = createItineraryFile(part); QDBusInterface remoteApp(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/MainApplication"), QStringLiteral("org.qtproject.Qt.QCoreApplication")); QVersionNumber kdeconnectVersion = QVersionNumber::fromString(remoteApp.property("applicationVersion").toString()); QString method; if (kdeconnectVersion >= QVersionNumber(1, 4, 0)) { method = QStringLiteral("openFile"); } else { method = QStringLiteral("shareUrl"); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), method); msg.setArguments({QUrl::fromLocalFile(fileName).toString()}); QDBusConnection::sessionBus().send(msg); } QString ItineraryUrlHandler::createItineraryFile(MimeTreeParser::Interface::BodyPart *part) const { QTemporaryFile f(QStringLiteral("XXXXXX.itinerary")); if (!f.open()) { qCWarning(ITINERARY_LOG) << "Failed to open temporary file:" << f.errorString(); return {}; } f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); KItinerary::File file(f.fileName()); if (!file.open(KItinerary::File::Write)) { qCWarning(ITINERARY_LOG) << "Failed to open itinerary bundle file:" << file.errorString(); return {}; } const auto m = memento(part); // add reservations const auto extractedData = m->data(); for (const auto &d : extractedData) { for (const auto &res : d.reservations) { file.addReservation(res); } } // add pkpass attachments for (const auto &passData : m->passData()) { file.addPass(KItinerary::File::passId(passData.passTypeIdentifier, passData.serialNumber), passData.rawData); } // add documents for (const auto &docData : m->documentData()) { file.addDocument(docData.docId, docData.docInfo, docData.rawData); } return f.fileName(); }