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"