1 /*
2     SPDX-FileCopyrightText: 2011-2013 Daniel Vrátil <dvratil@redhat.com>
3     SPDX-FileCopyrightText: 2020 Igor Poboiko <igor.poboiko@gmail.com>
4 
5     SPDX-License-Identifier: GPL-3.0-or-later
6 */
7 
8 #include "calendarhandler.h"
9 #include "defaultreminderattribute.h"
10 #include "googlecalendar_debug.h"
11 #include "googleresource.h"
12 #include "googlesettings.h"
13 
14 #include <Akonadi/Calendar/BlockAlarmsAttribute>
15 #include <Akonadi/CollectionColorAttribute>
16 #include <Akonadi/CollectionModifyJob>
17 #include <Akonadi/EntityDisplayAttribute>
18 #include <Akonadi/ItemModifyJob>
19 
20 #include <KGAPI/Account>
21 #include <KGAPI/Calendar/Calendar>
22 #include <KGAPI/Calendar/CalendarCreateJob>
23 #include <KGAPI/Calendar/CalendarDeleteJob>
24 #include <KGAPI/Calendar/CalendarFetchJob>
25 #include <KGAPI/Calendar/CalendarModifyJob>
26 #include <KGAPI/Calendar/Event>
27 #include <KGAPI/Calendar/EventCreateJob>
28 #include <KGAPI/Calendar/EventDeleteJob>
29 #include <KGAPI/Calendar/EventFetchJob>
30 #include <KGAPI/Calendar/EventModifyJob>
31 #include <KGAPI/Calendar/EventMoveJob>
32 #include <KGAPI/Calendar/FreeBusyQueryJob>
33 
34 #include <KCalendarCore/Calendar>
35 #include <KCalendarCore/FreeBusy>
36 #include <KCalendarCore/ICalFormat>
37 
38 using namespace KGAPI2;
39 using namespace Akonadi;
40 
mimeType()41 QString CalendarHandler::mimeType()
42 {
43     return KCalendarCore::Event::eventMimeType();
44 }
45 
canPerformTask(const Item & item)46 bool CalendarHandler::canPerformTask(const Item &item)
47 {
48     return GenericHandler::canPerformTask<KCalendarCore::Event::Ptr>(item);
49 }
50 
canPerformTask(const Item::List & items)51 bool CalendarHandler::canPerformTask(const Item::List &items)
52 {
53     return GenericHandler::canPerformTask<KCalendarCore::Event::Ptr>(items);
54 }
55 
setupCollection(Collection & collection,const CalendarPtr & calendar)56 void CalendarHandler::setupCollection(Collection &collection, const CalendarPtr &calendar)
57 {
58     collection.setContentMimeTypes({mimeType()});
59     collection.setName(calendar->uid());
60     collection.setRemoteId(calendar->uid());
61     if (calendar->editable()) {
62         collection.setRights(Collection::CanChangeCollection | Collection::CanDeleteCollection | Collection::CanCreateItem | Collection::CanChangeItem
63                              | Collection::CanDeleteItem);
64     } else {
65         collection.setRights(Collection::ReadOnly);
66     }
67     // TODO: for some reason, KOrganizer creates virtual collections
68     // newCollection.setVirtual(false);
69     // Setting icon
70     auto attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
71     attr->setDisplayName(calendar->title());
72     attr->setIconName(QStringLiteral("view-calendar"));
73     // Setting color
74     if (calendar->backgroundColor().isValid()) {
75         auto colorAttr = collection.attribute<CollectionColorAttribute>(Collection::AddIfMissing);
76         colorAttr->setColor(calendar->backgroundColor());
77     }
78     // Setting default reminders
79     auto reminderAttr = collection.attribute<DefaultReminderAttribute>(Collection::AddIfMissing);
80     reminderAttr->setReminders(calendar->defaultReminders());
81     // Block email reminders, since Google sends them for us
82     auto blockAlarms = collection.attribute<BlockAlarmsAttribute>(Collection::AddIfMissing);
83     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Audio, false);
84     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Display, false);
85     blockAlarms->blockAlarmType(KCalendarCore::Alarm::Procedure, false);
86 }
87 
retrieveCollections(const Collection & rootCollection)88 void CalendarHandler::retrieveCollections(const Collection &rootCollection)
89 {
90     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving calendars"));
91     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving calendars...";
92     auto job = new CalendarFetchJob(m_settings->accountPtr(), this);
93     connect(job, &CalendarFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job) {
94         if (!m_iface->handleError(job)) {
95             return;
96         }
97         qCDebug(GOOGLE_CALENDAR_LOG) << "Calendars retrieved";
98 
99         const ObjectsList calendars = qobject_cast<CalendarFetchJob *>(job)->items();
100         Collection::List collections;
101         collections.reserve(calendars.count());
102         const QStringList activeCalendars = m_settings->calendars();
103         for (const auto &object : calendars) {
104             const CalendarPtr &calendar = object.dynamicCast<Calendar>();
105             qCDebug(GOOGLE_CALENDAR_LOG) << " -" << calendar->title() << "(" << calendar->uid() << ")";
106             if (!activeCalendars.contains(calendar->uid())) {
107                 qCDebug(GOOGLE_CALENDAR_LOG) << "Skipping, not subscribed";
108                 continue;
109             }
110             Collection collection;
111             setupCollection(collection, calendar);
112             collection.setParentCollection(rootCollection);
113             collections << collection;
114         }
115 
116         m_iface->collectionsRetrievedFromHandler(collections);
117     });
118 }
119 
retrieveItems(const Collection & collection)120 void CalendarHandler::retrieveItems(const Collection &collection)
121 {
122     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving events for calendar" << collection.remoteId();
123     const QString syncToken = collection.remoteRevision();
124     auto job = new EventFetchJob(collection.remoteId(), m_settings->accountPtr(), this);
125     if (!syncToken.isEmpty()) {
126         qCDebug(GOOGLE_CALENDAR_LOG) << "Using sync token" << syncToken;
127         job->setSyncToken(syncToken);
128         job->setFetchDeleted(true);
129     } else {
130         // No need to fetch deleted items for non-incremental update
131         job->setFetchDeleted(false);
132         if (!m_settings->eventsSince().isEmpty()) {
133             const QDate date = QDate::fromString(m_settings->eventsSince(), Qt::ISODate);
134             job->setTimeMin(QDateTime(date.startOfDay()).toSecsSinceEpoch());
135         }
136     }
137 
138     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
139     connect(job, &EventFetchJob::finished, this, &CalendarHandler::slotItemsRetrieved);
140 
141     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving events for calendar '%1'", collection.displayName()));
142 }
143 
slotItemsRetrieved(KGAPI2::Job * job)144 void CalendarHandler::slotItemsRetrieved(KGAPI2::Job *job)
145 {
146     if (!m_iface->handleError(job)) {
147         return;
148     }
149     Item::List changedItems, removedItems;
150     auto collection = job->property(COLLECTION_PROPERTY).value<Collection>();
151     auto attr = collection.attribute<DefaultReminderAttribute>();
152 
153     auto fetchJob = qobject_cast<EventFetchJob *>(job);
154     const ObjectsList objects = fetchJob->items();
155     bool isIncremental = !fetchJob->syncToken().isEmpty();
156     qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieved" << objects.count() << "events for calendar" << collection.remoteId();
157     changedItems.reserve(objects.count());
158     for (const ObjectPtr &object : objects) {
159         const EventPtr event = object.dynamicCast<Event>();
160         if (event->useDefaultReminders() && attr) {
161             const KCalendarCore::Alarm::List alarms = attr->alarms(event.data());
162             for (const KCalendarCore::Alarm::Ptr &alarm : alarms) {
163                 event->addAlarm(alarm);
164             }
165         }
166 
167         Item item;
168         item.setMimeType(mimeType());
169         item.setParentCollection(collection);
170         item.setRemoteId(event->id());
171         item.setRemoteRevision(event->etag());
172         item.setPayload<KCalendarCore::Event::Ptr>(event.dynamicCast<KCalendarCore::Event>());
173 
174         if (event->deleted()) {
175             qCDebug(GOOGLE_CALENDAR_LOG) << " - removed" << event->uid();
176             removedItems << item;
177         } else {
178             qCDebug(GOOGLE_CALENDAR_LOG) << " - changed" << event->uid();
179             changedItems << item;
180         }
181     }
182 
183     if (!isIncremental) {
184         m_iface->itemsRetrieved(changedItems);
185     } else {
186         m_iface->itemsRetrievedIncremental(changedItems, removedItems);
187     }
188     qCDebug(GOOGLE_CALENDAR_LOG) << "Next sync token:" << fetchJob->syncToken();
189     collection.setRemoteRevision(fetchJob->syncToken());
190     new CollectionModifyJob(collection, this);
191 
192     emitReadyStatus();
193 }
194 
itemAdded(const Item & item,const Collection & collection)195 void CalendarHandler::itemAdded(const Item &item, const Collection &collection)
196 {
197     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding event to calendar '%1'", collection.name()));
198     qCDebug(GOOGLE_CALENDAR_LOG) << "Event added to calendar" << collection.remoteId();
199     EventPtr event(new Event(*item.payload<KCalendarCore::Event::Ptr>()));
200     auto job = new EventCreateJob(event, collection.remoteId(), m_settings->accountPtr(), this);
201     job->setSendUpdates(SendUpdatesPolicy::None);
202     connect(job, &EventCreateJob::finished, this, [this, item](KGAPI2::Job *job) {
203         if (!m_iface->handleError(job)) {
204             return;
205         }
206         Item newItem(item);
207         const EventPtr event = qobject_cast<EventCreateJob *>(job)->items().first().dynamicCast<Event>();
208         qCDebug(GOOGLE_CALENDAR_LOG) << "Event added";
209         newItem.setRemoteId(event->id());
210         newItem.setRemoteRevision(event->etag());
211         newItem.setGid(event->uid());
212         m_iface->itemChangeCommitted(newItem);
213         newItem.setPayload<KCalendarCore::Event::Ptr>(event.dynamicCast<KCalendarCore::Event>());
214         new ItemModifyJob(newItem, this);
215         emitReadyStatus();
216     });
217 }
218 
itemChanged(const Item & item,const QSet<QByteArray> &)219 void CalendarHandler::itemChanged(const Item &item, const QSet<QByteArray> & /*partIdentifiers*/)
220 {
221     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing event in calendar '%1'", item.parentCollection().displayName()));
222     qCDebug(GOOGLE_CALENDAR_LOG) << "Changing event" << item.remoteId();
223     EventPtr event(new Event(*item.payload<KCalendarCore::Event::Ptr>()));
224     auto job = new EventModifyJob(event, item.parentCollection().remoteId(), m_settings->accountPtr(), this);
225     job->setSendUpdates(SendUpdatesPolicy::None);
226     job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
227     connect(job, &EventModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished);
228 }
229 
itemsRemoved(const Item::List & items)230 void CalendarHandler::itemsRemoved(const Item::List &items)
231 {
232     m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 event", "Removing %1 events", items.count()));
233     QStringList eventIds;
234     eventIds.reserve(items.count());
235     std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), [](const Item &item) {
236         return item.remoteId();
237     });
238     qCDebug(GOOGLE_CALENDAR_LOG) << "Removing events:" << eventIds;
239     auto job = new EventDeleteJob(eventIds, items.first().parentCollection().remoteId(), m_settings->accountPtr(), this);
240     job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items));
241     connect(job, &EventDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished);
242 }
243 
itemsMoved(const Item::List & items,const Collection & collectionSource,const Collection & collectionDestination)244 void CalendarHandler::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination)
245 {
246     m_iface->emitStatus(AgentBase::Running,
247                         i18ncp("@info:status",
248                                "Moving %1 event from calendar '%2' to calendar '%3'",
249                                "Moving %1 events from calendar '%2' to calendar '%3'",
250                                items.count(),
251                                collectionSource.displayName(),
252                                collectionDestination.displayName()));
253     QStringList eventIds;
254     eventIds.reserve(items.count());
255     std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), [](const Item &item) {
256         return item.remoteId();
257     });
258     qCDebug(GOOGLE_CALENDAR_LOG) << "Moving events" << eventIds << "from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId();
259     auto job = new EventMoveJob(eventIds, collectionSource.remoteId(), collectionDestination.remoteId(), m_settings->accountPtr(), this);
260     job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items));
261     connect(job, &EventMoveJob::finished, this, &CalendarHandler::slotGenericJobFinished);
262 }
263 
collectionAdded(const Collection & collection,const Collection &)264 void CalendarHandler::collectionAdded(const Collection &collection, const Collection & /*parent*/)
265 {
266     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating calendar '%1'", collection.displayName()));
267     qCDebug(GOOGLE_CALENDAR_LOG) << "Adding calendar" << collection.displayName();
268     CalendarPtr calendar(new Calendar());
269     calendar->setTitle(collection.displayName());
270     calendar->setEditable(true);
271 
272     auto job = new CalendarCreateJob(calendar, m_settings->accountPtr(), this);
273     connect(job, &CalendarCreateJob::finished, this, [this, collection](KGAPI2::Job *job) {
274         if (!m_iface->handleError(job)) {
275             return;
276         }
277         CalendarPtr calendar = qobject_cast<CalendarCreateJob *>(job)->items().first().dynamicCast<Calendar>();
278         qCDebug(GOOGLE_CALENDAR_LOG) << "Created calendar" << calendar->uid();
279         // Enable newly added calendar in settings, otherwise user won't see it
280         m_settings->addCalendar(calendar->uid());
281         // TODO: the calendar returned by google is almost empty, i.e. it's not "editable",
282         // does not contain the color, etc
283         calendar->setEditable(true);
284         // Populate remoteId & other stuff
285         Collection newCollection(collection);
286         setupCollection(newCollection, calendar);
287         m_iface->collectionChangeCommitted(newCollection);
288         emitReadyStatus();
289     });
290 }
291 
collectionChanged(const Collection & collection)292 void CalendarHandler::collectionChanged(const Collection &collection)
293 {
294     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing calendar '%1'", collection.displayName()));
295     qCDebug(GOOGLE_CALENDAR_LOG) << "Changing calendar" << collection.remoteId();
296     CalendarPtr calendar(new Calendar());
297     calendar->setUid(collection.remoteId());
298     calendar->setTitle(collection.displayName());
299     calendar->setEditable(true);
300     auto job = new CalendarModifyJob(calendar, m_settings->accountPtr(), this);
301     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
302     connect(job, &CalendarModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished);
303 }
304 
collectionRemoved(const Collection & collection)305 void CalendarHandler::collectionRemoved(const Collection &collection)
306 {
307     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing calendar '%1'", collection.displayName()));
308     qCDebug(GOOGLE_CALENDAR_LOG) << "Removing calendar" << collection.remoteId();
309     auto job = new CalendarDeleteJob(collection.remoteId(), m_settings->accountPtr(), this);
310     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
311     connect(job, &CalendarDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished);
312 }
313 
314 /**
315  * FreeBusy
316  */
FreeBusyHandler(GoogleResourceStateInterface * iface,GoogleSettings * settings)317 FreeBusyHandler::FreeBusyHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings)
318     : m_iface(iface)
319     , m_settings(settings)
320 {
321 }
322 
lastCacheUpdate() const323 QDateTime FreeBusyHandler::lastCacheUpdate() const
324 {
325     return QDateTime();
326 }
327 
canHandleFreeBusy(const QString & email)328 void FreeBusyHandler::canHandleFreeBusy(const QString &email)
329 {
330     if (m_iface->canPerformTask()) {
331         m_iface->handlesFreeBusy(email, false);
332         return;
333     }
334 
335     auto job = new FreeBusyQueryJob(email, QDateTime::currentDateTimeUtc(), QDateTime::currentDateTimeUtc().addSecs(3600), m_settings->accountPtr(), this);
336     connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job) {
337         auto queryJob = qobject_cast<FreeBusyQueryJob *>(job);
338         if (!m_iface->handleError(job, false)) {
339             m_iface->handlesFreeBusy(queryJob->id(), false);
340             return;
341         }
342         m_iface->handlesFreeBusy(queryJob->id(), true);
343     });
344 }
345 
retrieveFreeBusy(const QString & email,const QDateTime & start,const QDateTime & end)346 void FreeBusyHandler::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end)
347 {
348     if (m_iface->canPerformTask()) {
349         m_iface->freeBusyRetrieved(email, QString(), false, QString());
350         return;
351     }
352 
353     auto job = new FreeBusyQueryJob(email, start, end, m_settings->accountPtr(), this);
354     connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job) {
355         auto queryJob = qobject_cast<FreeBusyQueryJob *>(job);
356 
357         if (!m_iface->handleError(job, false)) {
358             m_iface->freeBusyRetrieved(queryJob->id(), QString(), false, QString());
359             return;
360         }
361 
362         KCalendarCore::FreeBusy::Ptr fb(new KCalendarCore::FreeBusy);
363         fb->setUid(QStringLiteral("%1%2@google.com").arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMddTHHmmssZ"))));
364         fb->setOrganizer(job->account()->accountName());
365         fb->addAttendee(KCalendarCore::Attendee(QString(), queryJob->id()));
366         // FIXME: is it really sort?
367         fb->setDateTime(QDateTime::currentDateTimeUtc(), KCalendarCore::IncidenceBase::RoleSort);
368         const auto ranges = queryJob->busy();
369         for (const auto &range : ranges) {
370             fb->addPeriod(range.busyStart, range.busyEnd);
371         }
372 
373         KCalendarCore::ICalFormat format;
374         const QString fbStr = format.createScheduleMessage(fb, KCalendarCore::iTIPRequest);
375 
376         m_iface->freeBusyRetrieved(queryJob->id(), fbStr, true, QString());
377     });
378 }
379