File indexing completed on 2024-11-17 04:45:00

0001 /*
0002     SPDX-FileCopyrightText: 2011-2013 Daniel Vrátil <dvratil@redhat.com>
0003     SPDX-FileCopyrightText: 2020 Igor Poboiko <igor.poboiko@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-3.0-or-later
0006 */
0007 
0008 #include "calendarhandler.h"
0009 #include "defaultreminderattribute.h"
0010 #include "googlecalendar_debug.h"
0011 #include "googleresource.h"
0012 #include "googlesettings.h"
0013 
0014 #include <Akonadi/BlockAlarmsAttribute>
0015 #include <Akonadi/CollectionColorAttribute>
0016 #include <Akonadi/CollectionModifyJob>
0017 #include <Akonadi/EntityDisplayAttribute>
0018 #include <Akonadi/ItemModifyJob>
0019 
0020 #include <KGAPI/Account>
0021 #include <KGAPI/Calendar/Calendar>
0022 #include <KGAPI/Calendar/CalendarCreateJob>
0023 #include <KGAPI/Calendar/CalendarDeleteJob>
0024 #include <KGAPI/Calendar/CalendarFetchJob>
0025 #include <KGAPI/Calendar/CalendarModifyJob>
0026 #include <KGAPI/Calendar/Event>
0027 #include <KGAPI/Calendar/EventCreateJob>
0028 #include <KGAPI/Calendar/EventDeleteJob>
0029 #include <KGAPI/Calendar/EventFetchJob>
0030 #include <KGAPI/Calendar/EventModifyJob>
0031 #include <KGAPI/Calendar/EventMoveJob>
0032 #include <KGAPI/Calendar/FreeBusyQueryJob>
0033 
0034 #include <KCalendarCore/Calendar>
0035 #include <KCalendarCore/FreeBusy>
0036 #include <KCalendarCore/ICalFormat>
0037 
0038 using namespace KGAPI2;
0039 using namespace Akonadi;
0040 
0041 QString CalendarHandler::mimeType()
0042 {
0043     return KCalendarCore::Event::eventMimeType();
0044 }
0045 
0046 bool CalendarHandler::canPerformTask(const Item &item)
0047 {
0048     return GenericHandler::canPerformTask<KCalendarCore::Event::Ptr>(item);
0049 }
0050 
0051 bool CalendarHandler::canPerformTask(const Item::List &items)
0052 {
0053     return GenericHandler::canPerformTask<KCalendarCore::Event::Ptr>(items);
0054 }
0055 
0056 void CalendarHandler::setupCollection(Collection &collection, const CalendarPtr &calendar)
0057 {
0058     collection.setContentMimeTypes({mimeType()});
0059     collection.setName(calendar->uid());
0060     collection.setRemoteId(calendar->uid());
0061     if (calendar->editable()) {
0062         collection.setRights(Collection::CanChangeCollection | Collection::CanDeleteCollection | Collection::CanCreateItem | Collection::CanChangeItem
0063                              | Collection::CanDeleteItem);
0064     } else {
0065         collection.setRights(Collection::ReadOnly);
0066     }
0067     // TODO: for some reason, KOrganizer creates virtual collections
0068     // newCollection.setVirtual(false);
0069     // Setting icon
0070     auto attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
0071     attr->setDisplayName(calendar->title());
0072     attr->setIconName(QStringLiteral("view-calendar"));
0073     // Setting color
0074     if (calendar->backgroundColor().isValid()) {
0075         auto colorAttr = collection.attribute<CollectionColorAttribute>(Collection::AddIfMissing);
0076         colorAttr->setColor(calendar->backgroundColor());
0077     }
0078     // Setting default reminders
0079     auto reminderAttr = collection.attribute<DefaultReminderAttribute>(Collection::AddIfMissing);
0080     reminderAttr->setReminders(calendar->defaultReminders());
0081     // Block email reminders, since Google sends them for us
0082     auto blockAlarms = collection.attribute<BlockAlarmsAttribute>(Collection::AddIfMissing);
0083     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Audio, false);
0084     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Display, false);
0085     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Procedure, false);
0086 }
0087 
0088 void CalendarHandler::retrieveCollections(const Collection &rootCollection)
0089 {
0090     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving calendars"));
0091     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving calendars...";
0092     auto job = new CalendarFetchJob(m_settings->accountPtr(), this);
0093     connect(job, &CalendarFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job) {
0094         if (!m_iface->handleError(job)) {
0095             return;
0096         }
0097         qCDebug(GOOGLE_CALENDAR_LOG) << "Calendars retrieved";
0098 
0099         const ObjectsList calendars = qobject_cast<CalendarFetchJob *>(job)->items();
0100         Collection::List collections;
0101         collections.reserve(calendars.count());
0102         const QStringList activeCalendars = m_settings->calendars();
0103         for (const auto &object : calendars) {
0104             const CalendarPtr &calendar = object.dynamicCast<Calendar>();
0105             qCDebug(GOOGLE_CALENDAR_LOG) << " -" << calendar->title() << "(" << calendar->uid() << ")";
0106             if (!activeCalendars.contains(calendar->uid())) {
0107                 qCDebug(GOOGLE_CALENDAR_LOG) << "Skipping, not subscribed";
0108                 continue;
0109             }
0110             Collection collection;
0111             setupCollection(collection, calendar);
0112             collection.setParentCollection(rootCollection);
0113             collections << collection;
0114         }
0115 
0116         m_iface->collectionsRetrievedFromHandler(collections);
0117     });
0118 }
0119 
0120 void CalendarHandler::retrieveItems(const Collection &collection)
0121 {
0122     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving events for calendar" << collection.remoteId();
0123     const QString syncToken = collection.remoteRevision();
0124     auto job = new EventFetchJob(collection.remoteId(), m_settings->accountPtr(), this);
0125     if (!syncToken.isEmpty()) {
0126         qCDebug(GOOGLE_CALENDAR_LOG) << "Using sync token" << syncToken;
0127         job->setSyncToken(syncToken);
0128         job->setFetchDeleted(true);
0129     } else {
0130         // No need to fetch deleted items for non-incremental update
0131         job->setFetchDeleted(false);
0132         if (!m_settings->eventsSince().isEmpty()) {
0133             const QDate date = QDate::fromString(m_settings->eventsSince(), Qt::ISODate);
0134             job->setTimeMin(QDateTime(date.startOfDay()).toSecsSinceEpoch());
0135         }
0136     }
0137 
0138     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0139     connect(job, &EventFetchJob::finished, this, &CalendarHandler::slotItemsRetrieved);
0140 
0141     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving events for calendar '%1'", collection.displayName()));
0142 }
0143 
0144 void CalendarHandler::slotItemsRetrieved(KGAPI2::Job *job)
0145 {
0146     if (!m_iface->handleError(job)) {
0147         return;
0148     }
0149     Item::List changedItems, removedItems;
0150     auto collection = job->property(COLLECTION_PROPERTY).value<Collection>();
0151     auto attr = collection.attribute<DefaultReminderAttribute>();
0152 
0153     auto fetchJob = qobject_cast<EventFetchJob *>(job);
0154     const ObjectsList objects = fetchJob->items();
0155     bool isIncremental = !fetchJob->syncToken().isEmpty();
0156     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieved" << objects.count() << "events for calendar" << collection.remoteId();
0157     changedItems.reserve(objects.count());
0158     for (const ObjectPtr &object : objects) {
0159         const EventPtr event = object.dynamicCast<Event>();
0160         if (event->useDefaultReminders() && attr) {
0161             const KCalendarCore::Alarm::List alarms = attr->alarms(event.data());
0162             for (const KCalendarCore::Alarm::Ptr &alarm : alarms) {
0163                 event->addAlarm(alarm);
0164             }
0165         }
0166 
0167         Item item;
0168         item.setMimeType(mimeType());
0169         item.setParentCollection(collection);
0170         item.setRemoteId(event->id());
0171         item.setRemoteRevision(event->etag());
0172         item.setPayload<KCalendarCore::Event::Ptr>(event.dynamicCast<KCalendarCore::Event>());
0173 
0174         if (event->deleted()) {
0175             qCDebug(GOOGLE_CALENDAR_LOG) << " - removed" << event->uid();
0176             removedItems << item;
0177         } else {
0178             qCDebug(GOOGLE_CALENDAR_LOG) << " - changed" << event->uid();
0179             changedItems << item;
0180         }
0181     }
0182 
0183     if (!isIncremental) {
0184         m_iface->itemsRetrieved(changedItems);
0185     } else {
0186         m_iface->itemsRetrievedIncremental(changedItems, removedItems);
0187     }
0188     qCDebug(GOOGLE_CALENDAR_LOG) << "Next sync token:" << fetchJob->syncToken();
0189     collection.setRemoteRevision(fetchJob->syncToken());
0190     new CollectionModifyJob(collection, this);
0191 
0192     emitReadyStatus();
0193 }
0194 
0195 void CalendarHandler::itemAdded(const Item &item, const Collection &collection)
0196 {
0197     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding event to calendar '%1'", collection.name()));
0198     qCDebug(GOOGLE_CALENDAR_LOG) << "Event added to calendar" << collection.remoteId();
0199     EventPtr event(new Event(*item.payload<KCalendarCore::Event::Ptr>()));
0200     auto job = new EventCreateJob(event, collection.remoteId(), m_settings->accountPtr(), this);
0201     job->setSendUpdates(SendUpdatesPolicy::None);
0202     connect(job, &EventCreateJob::finished, this, [this, item](KGAPI2::Job *job) {
0203         if (!m_iface->handleError(job)) {
0204             return;
0205         }
0206         Item newItem(item);
0207         const EventPtr event = qobject_cast<EventCreateJob *>(job)->items().first().dynamicCast<Event>();
0208         qCDebug(GOOGLE_CALENDAR_LOG) << "Event added";
0209         newItem.setRemoteId(event->id());
0210         newItem.setRemoteRevision(event->etag());
0211         newItem.setGid(event->uid());
0212         m_iface->itemChangeCommitted(newItem);
0213         newItem.setPayload<KCalendarCore::Event::Ptr>(event.dynamicCast<KCalendarCore::Event>());
0214         new ItemModifyJob(newItem, this);
0215         emitReadyStatus();
0216     });
0217 }
0218 
0219 void CalendarHandler::itemChanged(const Item &item, const QSet<QByteArray> & /*partIdentifiers*/)
0220 {
0221     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing event in calendar '%1'", item.parentCollection().displayName()));
0222     qCDebug(GOOGLE_CALENDAR_LOG) << "Changing event" << item.remoteId();
0223     EventPtr event(new Event(*item.payload<KCalendarCore::Event::Ptr>()));
0224     auto job = new EventModifyJob(event, item.parentCollection().remoteId(), m_settings->accountPtr(), this);
0225     job->setSendUpdates(SendUpdatesPolicy::None);
0226     job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
0227     connect(job, &EventModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished);
0228 }
0229 
0230 void CalendarHandler::itemsRemoved(const Item::List &items)
0231 {
0232     m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 event", "Removing %1 events", items.count()));
0233     QStringList eventIds;
0234     eventIds.reserve(items.count());
0235     std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), [](const Item &item) {
0236         return item.remoteId();
0237     });
0238     qCDebug(GOOGLE_CALENDAR_LOG) << "Removing events:" << eventIds;
0239     auto job = new EventDeleteJob(eventIds, items.first().parentCollection().remoteId(), m_settings->accountPtr(), this);
0240     job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items));
0241     connect(job, &EventDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished);
0242 }
0243 
0244 void CalendarHandler::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination)
0245 {
0246     m_iface->emitStatus(AgentBase::Running,
0247                         i18ncp("@info:status",
0248                                "Moving %1 event from calendar '%2' to calendar '%3'",
0249                                "Moving %1 events from calendar '%2' to calendar '%3'",
0250                                items.count(),
0251                                collectionSource.displayName(),
0252                                collectionDestination.displayName()));
0253     QStringList eventIds;
0254     eventIds.reserve(items.count());
0255     std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), [](const Item &item) {
0256         return item.remoteId();
0257     });
0258     qCDebug(GOOGLE_CALENDAR_LOG) << "Moving events" << eventIds << "from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId();
0259     auto job = new EventMoveJob(eventIds, collectionSource.remoteId(), collectionDestination.remoteId(), m_settings->accountPtr(), this);
0260     job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items));
0261     connect(job, &EventMoveJob::finished, this, &CalendarHandler::slotGenericJobFinished);
0262 }
0263 
0264 void CalendarHandler::collectionAdded(const Collection &collection, const Collection & /*parent*/)
0265 {
0266     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating calendar '%1'", collection.displayName()));
0267     qCDebug(GOOGLE_CALENDAR_LOG) << "Adding calendar" << collection.displayName();
0268     CalendarPtr calendar(new Calendar());
0269     calendar->setTitle(collection.displayName());
0270     calendar->setEditable(true);
0271 
0272     auto job = new CalendarCreateJob(calendar, m_settings->accountPtr(), this);
0273     connect(job, &CalendarCreateJob::finished, this, [this, collection](KGAPI2::Job *job) {
0274         if (!m_iface->handleError(job)) {
0275             return;
0276         }
0277         CalendarPtr calendar = qobject_cast<CalendarCreateJob *>(job)->items().first().dynamicCast<Calendar>();
0278         qCDebug(GOOGLE_CALENDAR_LOG) << "Created calendar" << calendar->uid();
0279         // Enable newly added calendar in settings, otherwise user won't see it
0280         m_settings->addCalendar(calendar->uid());
0281         // TODO: the calendar returned by google is almost empty, i.e. it's not "editable",
0282         // does not contain the color, etc
0283         calendar->setEditable(true);
0284         // Populate remoteId & other stuff
0285         Collection newCollection(collection);
0286         setupCollection(newCollection, calendar);
0287         m_iface->collectionChangeCommitted(newCollection);
0288         emitReadyStatus();
0289     });
0290 }
0291 
0292 void CalendarHandler::collectionChanged(const Collection &collection)
0293 {
0294     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing calendar '%1'", collection.displayName()));
0295     qCDebug(GOOGLE_CALENDAR_LOG) << "Changing calendar" << collection.remoteId();
0296     CalendarPtr calendar(new Calendar());
0297     calendar->setUid(collection.remoteId());
0298     calendar->setTitle(collection.displayName());
0299     calendar->setEditable(true);
0300     auto job = new CalendarModifyJob(calendar, m_settings->accountPtr(), this);
0301     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0302     connect(job, &CalendarModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished);
0303 }
0304 
0305 void CalendarHandler::collectionRemoved(const Collection &collection)
0306 {
0307     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing calendar '%1'", collection.displayName()));
0308     qCDebug(GOOGLE_CALENDAR_LOG) << "Removing calendar" << collection.remoteId();
0309     auto job = new CalendarDeleteJob(collection.remoteId(), m_settings->accountPtr(), this);
0310     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0311     connect(job, &CalendarDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished);
0312 }
0313 
0314 /**
0315  * FreeBusy
0316  */
0317 FreeBusyHandler::FreeBusyHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings)
0318     : m_iface(iface)
0319     , m_settings(settings)
0320 {
0321 }
0322 
0323 QDateTime FreeBusyHandler::lastCacheUpdate() const
0324 {
0325     return {};
0326 }
0327 
0328 void FreeBusyHandler::canHandleFreeBusy(const QString &email)
0329 {
0330     if (m_iface->canPerformTask()) {
0331         m_iface->handlesFreeBusy(email, false);
0332         return;
0333     }
0334 
0335     auto job = new FreeBusyQueryJob(email, QDateTime::currentDateTimeUtc(), QDateTime::currentDateTimeUtc().addSecs(3600), m_settings->accountPtr(), this);
0336     connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job) {
0337         auto queryJob = qobject_cast<FreeBusyQueryJob *>(job);
0338         if (!m_iface->handleError(job, false)) {
0339             m_iface->handlesFreeBusy(queryJob->id(), false);
0340             return;
0341         }
0342         m_iface->handlesFreeBusy(queryJob->id(), true);
0343     });
0344 }
0345 
0346 void FreeBusyHandler::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end)
0347 {
0348     if (m_iface->canPerformTask()) {
0349         m_iface->freeBusyRetrieved(email, QString(), false, QString());
0350         return;
0351     }
0352 
0353     auto job = new FreeBusyQueryJob(email, start, end, m_settings->accountPtr(), this);
0354     connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job) {
0355         auto queryJob = qobject_cast<FreeBusyQueryJob *>(job);
0356 
0357         if (!m_iface->handleError(job, false)) {
0358             m_iface->freeBusyRetrieved(queryJob->id(), QString(), false, QString());
0359             return;
0360         }
0361 
0362         KCalendarCore::FreeBusy::Ptr fb(new KCalendarCore::FreeBusy);
0363         fb->setUid(QStringLiteral("%1%2@google.com").arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMddTHHmmssZ"))));
0364         fb->setOrganizer(job->account()->accountName());
0365         fb->addAttendee(KCalendarCore::Attendee(QString(), queryJob->id()));
0366         // FIXME: is it really sort?
0367         fb->setDateTime(QDateTime::currentDateTimeUtc(), KCalendarCore::IncidenceBase::RoleSort);
0368         const auto ranges = queryJob->busy();
0369         for (const auto &range : ranges) {
0370             fb->addPeriod(range.busyStart, range.busyEnd);
0371         }
0372 
0373         KCalendarCore::ICalFormat format;
0374         const QString fbStr = format.createScheduleMessage(fb, KCalendarCore::iTIPRequest);
0375 
0376         m_iface->freeBusyRetrieved(queryJob->id(), fbStr, true, QString());
0377     });
0378 }
0379 
0380 #include "moc_calendarhandler.cpp"