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