File indexing completed on 2025-01-19 04:46:50

0001 /*
0002    SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "itineraryurlhandler.h"
0008 #include "itinerary_debug.h"
0009 #include "itinerarykdeconnecthandler.h"
0010 #include "itinerarymemento.h"
0011 
0012 #include <MimeTreeParser/BodyPart>
0013 #include <MimeTreeParser/NodeHelper>
0014 
0015 #include <CalendarSupport/CalendarSingleton>
0016 
0017 #include <KItinerary/CalendarHandler>
0018 #include <KItinerary/File>
0019 #include <KItinerary/Reservation>
0020 
0021 #include <KCalendarCore/Event>
0022 
0023 #include <KIO/ApplicationLauncherJob>
0024 #include <KLocalizedString>
0025 #include <KService>
0026 
0027 #include <QDBusInterface>
0028 #include <QDate>
0029 #include <QIcon>
0030 #include <QMenu>
0031 #include <QStandardPaths>
0032 #include <QTemporaryFile>
0033 
0034 #include <memory>
0035 #include <type_traits>
0036 
0037 using namespace KItinerary;
0038 
0039 QString ItineraryUrlHandler::name() const
0040 {
0041     return QString::fromUtf8(staticMetaObject.className());
0042 }
0043 
0044 void ItineraryUrlHandler::setKDEConnectHandler(ItineraryKDEConnectHandler *kdeConnect)
0045 {
0046     m_kdeConnect = kdeConnect;
0047 }
0048 
0049 bool ItineraryUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const
0050 {
0051     Q_UNUSED(viewerInstance)
0052     const auto m = memento(part);
0053 
0054     if (path.startsWith(QLatin1StringView("semanticExpand?"))) {
0055         const auto idx = QStringView(path).mid(15).toInt();
0056         m->toggleExpanded(idx);
0057         const auto nodeHelper = part->nodeHelper();
0058         Q_EMIT nodeHelper->update(MimeTreeParser::Delayed);
0059         return true;
0060     }
0061 
0062     if (path == QLatin1StringView("showCalendar")) {
0063         showCalendar(m->startDate());
0064         return true;
0065     }
0066 
0067     if (path == QLatin1StringView("addToCalendar")) {
0068         addToCalendar(m);
0069         return true;
0070     }
0071 
0072     if (path == QLatin1StringView("import")) {
0073         openInApp(part);
0074         return true;
0075     }
0076 
0077     if (path == QLatin1StringView("sendToDeviceList")) {
0078         handleContextMenuRequest(part, path, QCursor::pos());
0079         return true;
0080     }
0081 
0082     if (path.startsWith(QLatin1StringView("sendToDevice-"))) {
0083         openWithKDEConnect(part, path.mid(13));
0084         return true;
0085     }
0086 
0087     return false;
0088 }
0089 
0090 bool ItineraryUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const
0091 {
0092     Q_UNUSED(part)
0093     if (path == QLatin1StringView("showCalendar") || path == QLatin1StringView("addToCalendar") || path == QLatin1StringView("import")
0094         || path.startsWith(QLatin1StringView("sendToDevice-"))) {
0095         // suppress default context menus for our buttons
0096         return true;
0097     }
0098 
0099     if (path != QLatin1StringView("sendToDeviceList")) {
0100         return false;
0101     }
0102 
0103     const auto m = memento(part);
0104     if (!m || !m->hasData()) {
0105         return false;
0106     }
0107 
0108     QMenu menu;
0109     QAction *action = nullptr;
0110     const auto devices = m_kdeConnect->devices();
0111     for (const auto &device : devices) {
0112         action = menu.addAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send to %1", device.name));
0113         QObject::connect(action, &QAction::triggered, this, [this, part, device]() {
0114             openWithKDEConnect(part, device.deviceId);
0115         });
0116     }
0117 
0118     menu.exec(p);
0119     return true;
0120 }
0121 
0122 QString ItineraryUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const
0123 {
0124     Q_UNUSED(part)
0125     if (path == QLatin1StringView("showCalendar")) {
0126         return i18n("Show calendar at the time of this reservation.");
0127     }
0128     if (path == QLatin1StringView("addToCalendar")) {
0129         return i18n("Add reservation to your calendar.");
0130     }
0131     if (path == QLatin1StringView("import")) {
0132         return i18n("Import reservation into KDE Itinerary.");
0133     }
0134     if (path.startsWith(QLatin1StringView("sendToDevice"))) {
0135         return i18n("Send this reservation to a device using KDE Connect.");
0136     }
0137     return {};
0138 }
0139 
0140 bool ItineraryUrlHandler::hasItineraryApp()
0141 {
0142     return KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary"));
0143 }
0144 
0145 ItineraryMemento *ItineraryUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const
0146 {
0147     const auto node = part->content()->topLevel();
0148     const auto nodeHelper = part->nodeHelper();
0149     if (!nodeHelper || !node) {
0150         return nullptr;
0151     }
0152     return dynamic_cast<ItineraryMemento *>(nodeHelper->bodyPartMemento(node->topLevel(), ItineraryMemento::identifier()));
0153 }
0154 
0155 void ItineraryUrlHandler::showCalendar(QDate date) const
0156 {
0157     // Start or activate KOrganizer. When Kontact is running it will switch to KOrganizer view
0158     const auto korgaService = KService::serviceByDesktopName(QStringLiteral("org.kde.korganizer"));
0159 
0160     if (!korgaService) {
0161         qCWarning(ITINERARY_LOG) << "Could not find KOrganizer";
0162         return;
0163     }
0164 
0165     // Open or activate KOrganizer. This will also activate Kontact if running
0166     auto job = new KIO::ApplicationLauncherJob(korgaService);
0167 
0168     connect(job, &KJob::finished, this, [date](KJob *job) {
0169         if (job->error()) {
0170             qCWarning(ITINERARY_LOG) << "failed to run korganizer" << job->errorString();
0171             return;
0172         }
0173 
0174         // select the date of the reservation
0175         QDBusInterface korgIface(QStringLiteral("org.kde.korganizer"),
0176                                  QStringLiteral("/Calendar"),
0177                                  QStringLiteral("org.kde.Korganizer.Calendar"),
0178                                  QDBusConnection::sessionBus());
0179         if (!korgIface.isValid()) {
0180             qCWarning(ITINERARY_LOG) << "Calendar interface is not valid! " << korgIface.lastError().message();
0181             return;
0182         }
0183         korgIface.call(QStringLiteral("showEventView"));
0184         korgIface.call(QStringLiteral("showDate"), date);
0185     });
0186 
0187     job->start();
0188 }
0189 
0190 static void attachPass(const KCalendarCore::Event::Ptr &event, const QList<QVariant> &reservations, ItineraryMemento *memento)
0191 {
0192     for (const auto &reservation : reservations) {
0193         if (!JsonLd::canConvert<Reservation>(reservation)) {
0194             return;
0195         }
0196 
0197         const auto res = JsonLd::convert<Reservation>(reservation);
0198         const auto data = memento->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber());
0199         if (data.isEmpty()) {
0200             return;
0201         }
0202 
0203         event->deleteAttachments(QStringLiteral("application/vnd.apple.pkpass"));
0204         using namespace KCalendarCore;
0205         Attachment att(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass"));
0206         att.setLabel(JsonLd::canConvert<FlightReservation>(reservation) ? i18n("Boarding Pass")
0207                                                                         : i18n("Ticket")); // TODO add passenger name after string freeze is lifted
0208         event->addAttachment(att);
0209     }
0210 }
0211 
0212 void ItineraryUrlHandler::addToCalendar(ItineraryMemento *memento) const
0213 {
0214     using namespace KCalendarCore;
0215 
0216     const auto calendar = CalendarSupport::calendarSingleton(true);
0217     const auto datas = memento->data();
0218     for (const auto &d : datas) {
0219         auto event = d.event;
0220         if (!event) {
0221             event.reset(new KCalendarCore::Event);
0222             CalendarHandler::fillEvent(d.reservations, event);
0223             if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) {
0224                 continue;
0225             }
0226             attachPass(event, d.reservations, memento);
0227             calendar->addEvent(event);
0228         } else {
0229             event->startUpdates();
0230             CalendarHandler::fillEvent(d.reservations, event);
0231             event->endUpdates();
0232             attachPass(event, d.reservations, memento);
0233             calendar->modifyIncidence(event);
0234         }
0235     }
0236 }
0237 
0238 void ItineraryUrlHandler::openInApp(MimeTreeParser::Interface::BodyPart *part) const
0239 {
0240     const auto fileName = createItineraryFile(part);
0241     auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary")));
0242     job->setUrls({QUrl::fromLocalFile(fileName)});
0243     job->start();
0244 }
0245 
0246 void ItineraryUrlHandler::openWithKDEConnect(MimeTreeParser::Interface::BodyPart *part, const QString &deviceId) const
0247 {
0248     const auto fileName = createItineraryFile(part);
0249     m_kdeConnect->sendToDevice(fileName, deviceId);
0250 }
0251 
0252 QString ItineraryUrlHandler::createItineraryFile(MimeTreeParser::Interface::BodyPart *part) const
0253 {
0254     QTemporaryFile f(QStringLiteral("XXXXXX.itinerary"));
0255     if (!f.open()) {
0256         qCWarning(ITINERARY_LOG) << "Failed to open temporary file:" << f.errorString();
0257         return {};
0258     }
0259     f.close();
0260     part->nodeHelper()->addTempFile(f.fileName());
0261     f.setAutoRemove(false);
0262 
0263     KItinerary::File file(f.fileName());
0264     if (!file.open(KItinerary::File::Write)) {
0265         qCWarning(ITINERARY_LOG) << "Failed to open itinerary bundle file:" << file.errorString();
0266         return {};
0267     }
0268 
0269     const auto m = memento(part);
0270 
0271     // add reservations
0272     const auto extractedData = m->data();
0273     for (const auto &d : extractedData) {
0274         for (const auto &res : d.reservations) {
0275             file.addReservation(res);
0276         }
0277     }
0278 
0279     // add pkpass attachments
0280     for (const auto &passData : m->passData()) {
0281         file.addPass(KItinerary::File::passId(passData.passTypeIdentifier, passData.serialNumber), passData.rawData);
0282     }
0283 
0284     // add documents
0285     for (const auto &docData : m->documentData()) {
0286         file.addDocument(docData.docId, docData.docInfo, docData.rawData);
0287     }
0288 
0289     return f.fileName();
0290 }
0291 
0292 #include "moc_itineraryurlhandler.cpp"