Kgapi

calendarservice.cpp
1/*
2 * This file is part of LibKGAPI library
3 *
4 * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8
9#include "calendarservice.h"
10#include "calendar.h"
11#include "debug.h"
12#include "reminder.h"
13#include "utils.h"
14
15#include <KCalendarCore/Alarm>
16#include <KCalendarCore/Attendee>
17#include <KCalendarCore/Event>
18#include <KCalendarCore/ICalFormat>
19#include <KCalendarCore/Person>
20#include <KCalendarCore/Recurrence>
21#include <KCalendarCore/RecurrenceRule>
22
23#include <QJsonDocument>
24#include <QNetworkRequest>
25#include <QTimeZone>
26#include <QUrlQuery>
27#include <QVariant>
28
29#include <map>
30#include <memory>
31
32namespace KGAPI2
33{
34
36{
37
38namespace Private
39{
40KCalendarCore::DateList parseRDate(const QString &rule);
41
42ObjectPtr JSONToCalendar(const QVariantMap &data);
43ObjectPtr JSONToEvent(const QVariantMap &data, const QString &timezone = QString());
44
45/**
46 * Checks whether TZID is in Olson format and converts it to it if necessary
47 *
48 * This is mainly to handle crazy Microsoft TZIDs like
49 * "(GMT) Greenwich Mean Time/Dublin/Edinburgh/London", because Google only
50 * accepts TZIDs in Olson format ("Europe/London").
51 *
52 * It first tries to match the given \p tzid to all TZIDs in KTimeZones::zones().
53 * If it fails, it parses the \p event, looking for X-MICROSOFT-CDO-TZID
54 * property and than matches it to Olson-formatted TZID using a table.
55 *
56 * When the method fails to process the TZID, it returns the original \p tzid
57 * in hope, that Google will cope with it.
58 */
59QString checkAndConverCDOTZID(const QString &tzid, const EventPtr &event);
60
61static const QUrl GoogleApisUrl(QStringLiteral("https://www.googleapis.com"));
62static const QString CalendarListBasePath(QStringLiteral("/calendar/v3/users/me/calendarList"));
63static const QString CalendarBasePath(QStringLiteral("/calendar/v3/calendars"));
64}
65
67{
68 QNetworkRequest request(url);
69 request.setRawHeader("GData-Version", CalendarService::APIVersion().toLatin1());
70
71 return request;
72}
73
74/************* URLS **************/
75
77{
78 QUrl url(Private::GoogleApisUrl);
79 url.setPath(Private::CalendarListBasePath);
80 return url;
81}
82
83QUrl fetchCalendarUrl(const QString &calendarID)
84{
85 QUrl url(Private::GoogleApisUrl);
86 url.setPath(Private::CalendarListBasePath % QLatin1Char('/') % calendarID);
87 return url;
88}
89
90QUrl updateCalendarUrl(const QString &calendarID)
91{
92 QUrl url(Private::GoogleApisUrl);
93 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
94 return url;
95}
96
98{
99 QUrl url(Private::GoogleApisUrl);
100 url.setPath(Private::CalendarBasePath);
101 return url;
102}
103
105{
106 QUrl url(Private::GoogleApisUrl);
107 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
108 return url;
109}
110
111QUrl fetchEventsUrl(const QString &calendarID)
112{
113 QUrl url(Private::GoogleApisUrl);
114 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events"));
115 return url;
116}
117
118QUrl fetchEventUrl(const QString &calendarID, const QString &eventID)
119{
120 QUrl url(Private::GoogleApisUrl);
121 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
122 return url;
123}
124
125namespace
126{
127
128QString sendUpdatesPolicyToString(SendUpdatesPolicy policy)
129{
130 switch (policy) {
131 case SendUpdatesPolicy::All:
132 return QStringLiteral("all");
134 return QStringLiteral("externalOnly");
136 return QStringLiteral("none");
137 }
138 Q_UNREACHABLE();
139}
140
141static const QString sendUpatesQueryParam = QStringLiteral("sendUpdates");
142static const QString destinationQueryParam = QStringLiteral("destination");
143}
144
145QUrl updateEventUrl(const QString &calendarID, const QString &eventID, SendUpdatesPolicy updatePolicy)
146{
147 QUrl url(Private::GoogleApisUrl);
148 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
149 QUrlQuery query(url);
150 query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
151 url.setQuery(query);
152 return url;
153}
154
155QUrl createEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
156{
157 QUrl url(Private::GoogleApisUrl);
158 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events"));
159 QUrlQuery query(url);
160 query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
161 url.setQuery(query);
162 return url;
163}
164
165QUrl importEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
166{
167 QUrl url(Private::GoogleApisUrl);
168 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events") % QLatin1StringView("/import"));
169 QUrlQuery query(url);
170 query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
171 url.setQuery(query);
172 return url;
173}
174
175QUrl removeEventUrl(const QString &calendarID, const QString &eventID)
176{
177 QUrl url(Private::GoogleApisUrl);
178 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
179 return url;
180}
181
182QUrl moveEventUrl(const QString &sourceCalendar, const QString &destCalendar, const QString &eventID)
183{
184 QUrl url(Private::GoogleApisUrl);
185 url.setPath(Private::CalendarBasePath % QLatin1Char('/') % sourceCalendar % QLatin1StringView("/events/") % eventID % QLatin1StringView("/move"));
186 QUrlQuery query(url);
187 query.addQueryItem(destinationQueryParam, destCalendar);
188 url.setQuery(query);
189 return url;
190}
191
193{
194 QUrl url(Private::GoogleApisUrl);
195 url.setPath(QStringLiteral("/calendar/v3/freeBusy"));
196 return url;
197}
198
199namespace
200{
201
202static const auto kindParam = QStringLiteral("kind");
203static const auto idParam = QStringLiteral("id");
204static const auto etagParam = QStringLiteral("etag");
205
206static const auto nextSyncTokenParam = QStringLiteral("nextSyncToken");
207static const auto nextPageTokenParam = QStringLiteral("nextPageToken");
208static const auto pageTokenParam = QStringLiteral("pageToken");
209static const auto itemsParam = QStringLiteral("items");
210
211static const auto calendarSummaryParam = QStringLiteral("summary");
212static const auto calendarDescriptionParam = QStringLiteral("description");
213static const auto calendarLocationParam = QStringLiteral("location");
214static const auto calendarTimezoneParam = QStringLiteral("timeZone");
215static const auto calendarBackgroundColorParam = QStringLiteral("backgroundColor");
216static const auto calendarForegroundColorParam = QStringLiteral("foregroundColor");
217static const auto calendarAccessRoleParam = QStringLiteral("accessRole");
218static const auto calendarDefaultRemindersParam = QStringLiteral("defaultReminders");
219static const auto reminderMethodParam = QStringLiteral("method");
220static const auto reminderMinutesParam = QStringLiteral("minutes");
221
222static const auto eventiCalUIDParam = QStringLiteral("iCalUID");
223static const auto eventStatusParam = QStringLiteral("status");
224static const auto eventCreatedParam = QStringLiteral("created");
225static const auto eventUpdatedParam = QStringLiteral("updated");
226static const auto eventSummaryParam = QStringLiteral("summary");
227static const auto eventDescriptionParam = QStringLiteral("description");
228static const auto eventLocationParam = QStringLiteral("location");
229static const auto eventStartPram = QStringLiteral("start");
230static const auto eventEndParam = QStringLiteral("end");
231static const auto eventOriginalStartTimeParam = QStringLiteral("originalStartTime");
232static const auto eventTransparencyParam = QStringLiteral("transparency");
233static const auto eventOrganizerParam = QStringLiteral("organizer");
234static const auto eventAttendeesParam = QStringLiteral("attendees");
235static const auto eventRecurrenceParam = QStringLiteral("recurrence");
236static const auto eventRemindersParam = QStringLiteral("reminders");
237static const auto eventExtendedPropertiesParam = QStringLiteral("extendedProperties");
238static const auto eventRecurringEventIdParam = QStringLiteral("recurringEventId");
239
240static const auto attendeeDisplayNameParam = QStringLiteral("displayName");
241static const auto attendeeEmailParam = QStringLiteral("email");
242static const auto attendeeResponseStatusParam = QStringLiteral("responseStatus");
243static const auto attendeeOptionalParam = QStringLiteral("optional");
244
245static const auto organizerDisplayNameParam = QStringLiteral("displayName");
246static const auto organizerEmailParam = QStringLiteral("email");
247
248static const auto reminderUseDefaultParam = QStringLiteral("useDefault");
249static const auto reminderOverridesParam = QStringLiteral("overrides");
250
251static const auto propertyPrivateParam = QStringLiteral("private");
252static const auto propertySharedParam = QStringLiteral("shared");
253
254static const auto dateParam = QStringLiteral("date");
255static const auto dateTimeParam = QStringLiteral("dateTime");
256static const auto timeZoneParam = QStringLiteral("timeZone");
257
258static const auto calendarListKind = QLatin1StringView("calendar#calendarList");
259static const auto calendarListEntryKind = QLatin1StringView("calendar#calendarListEntry");
260static const auto calendarKind = QLatin1StringView("calendar#calendar");
261static const auto eventKind = QLatin1StringView("calendar#event");
262static const auto eventsKind = QLatin1StringView("calendar#events");
263
264static const auto writerAccessRole = QLatin1StringView("writer");
265static const auto ownerAccessRole = QLatin1StringView("owner");
266static const auto emailMethod = QLatin1StringView("email");
267static const auto popupMethod = QLatin1StringView("popup");
268
269static const auto confirmedStatus = QLatin1StringView("confirmed");
270static const auto canceledStatus = QLatin1StringView("cancelled");
271static const auto tentativeStatus = QLatin1StringView("tentative");
272static const auto acceptedStatus = QLatin1StringView("accepted");
273static const auto needsActionStatus = QLatin1StringView("needsAction");
274static const auto transparentTransparency = QLatin1StringView("transparent");
275static const auto opaqueTransparency = QLatin1StringView("opaque");
276static const auto declinedStatus = QLatin1StringView("declined");
277static const auto categoriesProperty = QLatin1StringView("categories");
278
279static const auto hangoutLinkParam = QStringLiteral("hangoutLink");
280
281static const auto eventTypeParam = QStringLiteral("eventType");
282
283}
284
286{
287 return QStringLiteral("3");
288}
289
290CalendarPtr JSONToCalendar(const QByteArray &jsonData)
291{
292 const auto document = QJsonDocument::fromJson(jsonData);
293 const auto calendar = document.toVariant().toMap();
294
295 if (calendar.value(kindParam).toString() != calendarListEntryKind && calendar.value(kindParam).toString() != calendarKind) {
296 return CalendarPtr();
297 }
298
299 return Private::JSONToCalendar(calendar).staticCast<Calendar>();
300}
301
302ObjectPtr Private::JSONToCalendar(const QVariantMap &data)
303{
304 auto calendar = CalendarPtr::create();
305
306 const auto id = QUrl::fromPercentEncoding(data.value(idParam).toByteArray());
307 calendar->setUid(id);
308 calendar->setEtag(data.value(etagParam).toString());
309 calendar->setTitle(data.value(calendarSummaryParam).toString());
310 calendar->setDetails(data.value(calendarDescriptionParam).toString());
311 calendar->setLocation(data.value(calendarLocationParam).toString());
312 calendar->setTimezone(data.value(calendarTimezoneParam).toString());
313 calendar->setBackgroundColor(QColor(data.value(calendarBackgroundColorParam).toString()));
314 calendar->setForegroundColor(QColor(data.value(calendarForegroundColorParam).toString()));
315
316 if ((data.value(calendarAccessRoleParam).toString() == writerAccessRole) || (data.value(calendarAccessRoleParam).toString() == ownerAccessRole)) {
317 calendar->setEditable(true);
318 } else {
319 calendar->setEditable(false);
320 }
321
322 const auto reminders = data.value(calendarDefaultRemindersParam).toList();
323 for (const auto &r : reminders) {
324 const auto reminder = r.toMap();
325
326 auto rem = ReminderPtr::create();
327 if (reminder.value(reminderMethodParam).toString() == emailMethod) {
328 rem->setType(KCalendarCore::Alarm::Email);
329 } else if (reminder.value(reminderMethodParam).toString() == popupMethod) {
330 rem->setType(KCalendarCore::Alarm::Display);
331 } else {
332 rem->setType(KCalendarCore::Alarm::Invalid);
333 }
334
335 rem->setStartOffset(KCalendarCore::Duration(reminder.value(reminderMinutesParam).toInt() * (-60)));
336
337 calendar->addDefaultReminer(rem);
338 }
339
340 return calendar.dynamicCast<Object>();
341}
342
343QByteArray calendarToJSON(const CalendarPtr &calendar)
344{
345 QVariantMap entry;
346
347 if (!calendar->uid().isEmpty()) {
348 entry.insert(idParam, calendar->uid());
349 }
350
351 entry.insert(calendarSummaryParam, calendar->title());
352 entry.insert(calendarDescriptionParam, calendar->details());
353 entry.insert(calendarLocationParam, calendar->location());
354 if (!calendar->timezone().isEmpty()) {
355 entry.insert(calendarTimezoneParam, calendar->timezone());
356 }
357
358 const auto document = QJsonDocument::fromVariant(entry);
359 return document.toJson(QJsonDocument::Compact);
360}
361
362ObjectsList parseCalendarJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
363{
364 const auto document = QJsonDocument::fromJson(jsonFeed);
365 const auto data = document.toVariant().toMap();
366
367 ObjectsList list;
368
369 if (data.value(kindParam).toString() == calendarListKind) {
370 if (data.contains(nextPageTokenParam)) {
371 feedData.nextPageUrl = fetchCalendarsUrl();
372 QUrlQuery query(feedData.nextPageUrl);
373 query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
374 feedData.nextPageUrl.setQuery(query);
375 }
376 } else {
377 return {};
378 }
379
380 const auto items = data.value(itemsParam).toList();
381 list.reserve(items.size());
382 for (const auto &i : items) {
383 list.push_back(Private::JSONToCalendar(i.toMap()));
384 }
385
386 return list;
387}
388
389EventPtr JSONToEvent(const QByteArray &jsonData)
390{
391 QJsonParseError error;
392 QJsonDocument document = QJsonDocument::fromJson(jsonData, &error);
393 if (error.error != QJsonParseError::NoError) {
394 qCWarning(KGAPIDebug) << "Error parsing event JSON: " << error.errorString();
395 }
396 QVariantMap data = document.toVariant().toMap();
397 if (data.value(kindParam).toString() != eventKind) {
398 return EventPtr();
399 }
400
401 return Private::JSONToEvent(data).staticCast<Event>();
402}
403
404namespace
405{
406
407struct ParsedDt {
408 QDateTime dt;
409 bool isAllDay;
410};
411
412ParsedDt parseDt(const QVariantMap &data, const QString &timezone, bool isDtEnd)
413{
414 if (data.contains(dateParam)) {
415 auto dt = QDateTime::fromString(data.value(dateParam).toString(), Qt::ISODate);
416 if (isDtEnd) {
417 // Google reports all-day events to end on the next day, e.g. a
418 // Monday all-day event will be reporting as starting on Monday and
419 // ending on Tuesday, while KCalendarCore/iCal uses the same day for
420 // dtEnd, so adjust the end date here.
421 dt = dt.addDays(-1);
422 }
423 return {dt, true};
424 } else if (data.contains(dateTimeParam)) {
425 auto dt = Utils::rfc3339DateFromString(data.value(dateTimeParam).toString());
426 // If there's a timezone specified in the "start" entity, then use it
427 if (data.contains(timeZoneParam)) {
428 const QTimeZone tz = QTimeZone(data.value(timeZoneParam).toString().toUtf8());
429 if (tz.isValid()) {
430 dt = dt.toTimeZone(tz);
431 } else {
432 qCWarning(KGAPIDebug) << "Invalid timezone" << data.value(timeZoneParam).toString();
433 }
434
435 // Otherwise try to fallback to calendar-wide timezone
436 } else if (!timezone.isEmpty()) {
437 const QTimeZone tz(timezone.toUtf8());
438 if (tz.isValid()) {
439 dt.setTimeZone(tz);
440 } else {
441 qCWarning(KGAPIDebug) << "Invalid timezone" << timezone;
442 }
443 }
444 return {dt, false};
445 } else {
446 return {{}, false};
447 }
448}
449
450void setEventCategories(EventPtr &event, const QVariantMap &properties)
451{
452 for (auto iter = properties.cbegin(), end = properties.cend(); iter != end; ++iter) {
453 if (iter.key() == categoriesProperty) {
454 event->setCategories(iter.value().toString());
455 }
456 }
457}
458
459} // namespace
460
461ObjectPtr Private::JSONToEvent(const QVariantMap &data, const QString &timezone)
462{
463 auto event = EventPtr::create();
464
465 event->setId(data.value(idParam).toString());
466 event->setHangoutLink(data.value(hangoutLinkParam).toString());
467 event->setUid(data.value(eventiCalUIDParam).toString());
468 event->setEtag(data.value(etagParam).toString());
469
470 if (data.value(eventStatusParam).toString() == confirmedStatus) {
472 } else if (data.value(eventStatusParam).toString() == canceledStatus) {
474 event->setDeleted(true);
475 } else if (data.value(eventStatusParam).toString() == tentativeStatus) {
477 } else {
478 event->setStatus(KCalendarCore::Incidence::StatusNone);
479 }
480
481 event->setCreated(Utils::rfc3339DateFromString(data.value(eventCreatedParam).toString()));
482 event->setLastModified(Utils::rfc3339DateFromString(data.value(eventUpdatedParam).toString()));
483 event->setSummary(data.value(eventSummaryParam).toString());
484 event->setDescription(data.value(eventDescriptionParam).toString());
485 event->setLocation(data.value(eventLocationParam).toString());
486
487 const auto dtStart = parseDt(data.value(eventStartPram).toMap(), timezone, false);
488 event->setDtStart(dtStart.dt);
489 event->setAllDay(dtStart.isAllDay);
490
491 const auto dtEnd = parseDt(data.value(eventEndParam).toMap(), timezone, true);
492 event->setDtEnd(dtEnd.dt);
493
494 if (data.contains(eventOriginalStartTimeParam)) {
495 const auto recurrenceId = parseDt(data.value(eventOriginalStartTimeParam).toMap(), timezone, false);
496 event->setRecurrenceId(recurrenceId.dt);
497 }
498
499 if (data.value(eventTransparencyParam).toString() == transparentTransparency) {
500 event->setTransparency(Event::Transparent);
501 } else { /* Assume opaque as default transparency */
502 event->setTransparency(Event::Opaque);
503 }
504
505 const auto attendees = data.value(eventAttendeesParam).toList();
506 for (const auto &a : attendees) {
507 const auto att = a.toMap();
508 KCalendarCore::Attendee attendee(att.value(attendeeDisplayNameParam).toString(), att.value(attendeeEmailParam).toString());
509 const auto responseStatus = att.value(attendeeResponseStatusParam).toString();
510 if (responseStatus == acceptedStatus) {
511 attendee.setStatus(KCalendarCore::Attendee::Accepted);
512 } else if (responseStatus == declinedStatus) {
513 attendee.setStatus(KCalendarCore::Attendee::Declined);
514 } else if (responseStatus == tentativeStatus) {
515 attendee.setStatus(KCalendarCore::Attendee::Tentative);
516 } else {
517 attendee.setStatus(KCalendarCore::Attendee::NeedsAction);
518 }
519
520 if (att.value(attendeeOptionalParam).toBool()) {
522 }
523 const auto uid = att.value(idParam).toString();
524 if (!uid.isEmpty()) {
525 attendee.setUid(uid);
526 } else {
527 // Set some UID, just so that the results are reproducible
528 attendee.setUid(QString::number(qHash(attendee.email())));
529 }
530 event->addAttendee(attendee, true);
531 }
532
533 /* According to RFC, only events with attendees can have an organizer.
534 * Google seems to ignore it, so we must take care of it here */
535 if (event->attendeeCount() > 0) {
536 KCalendarCore::Person organizer;
537 const auto organizerData = data.value(eventOrganizerParam).toMap();
538 organizer.setName(organizerData.value(organizerDisplayNameParam).toString());
539 organizer.setEmail(organizerData.value(organizerEmailParam).toString());
540 event->setOrganizer(organizer);
541 }
542
543 const QStringList recrs = data.value(eventRecurrenceParam).toStringList();
544 for (const QString &rec : recrs) {
545 KCalendarCore::ICalFormat format;
546 const QStringView recView(rec);
547 if (recView.left(5) == QLatin1StringView("RRULE")) {
548 auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
549 const auto ok = format.fromString(recurrenceRule.get(), rec.mid(6));
550 Q_UNUSED(ok)
551 recurrenceRule->setRRule(rec);
552 event->recurrence()->addRRule(recurrenceRule.release());
553 } else if (recView.left(6) == QLatin1StringView("EXRULE")) {
554 auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
555 const auto ok = format.fromString(recurrenceRule.get(), rec.mid(7));
556 Q_UNUSED(ok)
557 recurrenceRule->setRRule(rec);
558 event->recurrence()->addExRule(recurrenceRule.release());
559 } else if (recView.left(6) == QLatin1StringView("EXDATE")) {
560 KCalendarCore::DateList exdates = Private::parseRDate(rec);
561 event->recurrence()->setExDates(exdates);
562 } else if (recView.left(5) == QLatin1StringView("RDATE")) {
563 KCalendarCore::DateList rdates = Private::parseRDate(rec);
564 event->recurrence()->setRDates(rdates);
565 }
566 }
567
568 const auto reminders = data.value(eventRemindersParam).toMap();
569 if (reminders.contains(reminderUseDefaultParam) && reminders.value(reminderUseDefaultParam).toBool()) {
570 event->setUseDefaultReminders(true);
571 } else {
572 event->setUseDefaultReminders(false);
573 }
574
575 const auto overrides = reminders.value(reminderOverridesParam).toList();
576 for (const auto &r : overrides) {
577 const auto reminderOverride = r.toMap();
578 auto alarm = KCalendarCore::Alarm::Ptr::create(static_cast<KCalendarCore::Incidence *>(event.data()));
579 alarm->setTime(event->dtStart());
580
581 if (reminderOverride.value(reminderMethodParam).toString() == popupMethod) {
582 alarm->setType(KCalendarCore::Alarm::Display);
583 } else if (reminderOverride.value(reminderMethodParam).toString() == emailMethod) {
584 alarm->setType(KCalendarCore::Alarm::Email);
585 } else {
586 alarm->setType(KCalendarCore::Alarm::Invalid);
587 continue;
588 }
589
590 alarm->setStartOffset(KCalendarCore::Duration(reminderOverride.value(reminderMinutesParam).toInt() * (-60)));
591 alarm->setEnabled(true);
592 event->addAlarm(alarm);
593 }
594
595 const auto extendedProperties = data.value(eventExtendedPropertiesParam).toMap();
596 setEventCategories(event, extendedProperties.value(propertyPrivateParam).toMap());
597 setEventCategories(event, extendedProperties.value(propertySharedParam).toMap());
598
599 if (const auto eventType = data.value(eventTypeParam).toString(); !eventType.isEmpty()) {
600 event->setEventType(eventTypeFromString(eventType));
601 } else {
602 event->setEventType(Event::EventType::Default);
603 }
604
605 return event.dynamicCast<Object>();
606}
607
608namespace
609{
610
611enum class SerializeDtFlag { AllDay = 1 << 0, IsDtEnd = 1 << 1, HasRecurrence = 1 << 2 };
612using SerializeDtFlags = QFlags<SerializeDtFlag>;
613
614QVariantMap serializeDt(const EventPtr &event, const QDateTime &dt, SerializeDtFlags flags)
615{
616 QVariantMap rv;
617 if (flags & SerializeDtFlag::AllDay) {
618 /* For Google, all-day events starts on Monday and ends on Tuesday,
619 * while in KDE, it both starts and ends on Monday. */
620 const auto adjusted = dt.addDays((flags & SerializeDtFlag::IsDtEnd) ? 1 : 0);
621 rv.insert(dateParam, adjusted.toString(QStringLiteral("yyyy-MM-dd")));
622 } else {
623 rv.insert(dateTimeParam, Utils::rfc3339DateToString(dt));
624 QString tzEnd = QString::fromUtf8(dt.timeZone().id());
625 if (flags & SerializeDtFlag::HasRecurrence && tzEnd.isEmpty()) {
626 tzEnd = QString::fromUtf8(QTimeZone::utc().id());
627 }
628 if (!tzEnd.isEmpty()) {
629 rv.insert(timeZoneParam, Private::checkAndConverCDOTZID(tzEnd, event));
630 }
631 }
632
633 return rv;
634}
635
636} // namespace
637
638QByteArray eventToJSON(const EventPtr &event, EventSerializeFlags flags)
639{
640 QVariantMap data;
641
642 data.insert(kindParam, eventKind);
643
644 if (!(flags & EventSerializeFlag::NoID)) {
645 data.insert(idParam, event->id());
646 }
647
648 data.insert(eventiCalUIDParam, event->uid());
649
650 if (event->status() == KCalendarCore::Incidence::StatusConfirmed) {
651 data.insert(eventStatusParam, confirmedStatus);
652 } else if (event->status() == KCalendarCore::Incidence::StatusCanceled) {
653 data.insert(eventStatusParam, canceledStatus);
654 } else if (event->status() == KCalendarCore::Incidence::StatusTentative) {
655 data.insert(eventStatusParam, tentativeStatus);
656 }
657
658 data.insert(eventSummaryParam, event->summary());
659 data.insert(eventDescriptionParam, event->description());
660 data.insert(eventLocationParam, event->location());
661
662 QVariantList recurrence;
664 const auto exRules = event->recurrence()->exRules();
665 const auto rRules = event->recurrence()->rRules();
666 recurrence.reserve(rRules.size() + rRules.size() + 2);
667 for (KCalendarCore::RecurrenceRule *rRule : rRules) {
668 recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
669 }
670 for (KCalendarCore::RecurrenceRule *rRule : exRules) {
671 recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
672 }
673
674 QStringList dates;
675 const auto rDates = event->recurrence()->rDates();
676 dates.reserve(rDates.size());
677 for (const auto &rDate : rDates) {
678 dates.push_back(rDate.toString(QStringLiteral("yyyyMMdd")));
679 }
680
681 if (!dates.isEmpty()) {
682 recurrence.push_back(QString(QStringLiteral("RDATE;VALUE=DATA:") + dates.join(QLatin1Char(','))));
683 }
684
685 dates.clear();
686 const auto exDates = event->recurrence()->exDates();
687 dates.reserve(exDates.size());
688 for (const auto &exDate : exDates) {
689 dates.push_back(exDate.toString(QStringLiteral("yyyyMMdd")));
690 }
691
692 if (!dates.isEmpty()) {
693 recurrence.push_back(QString(QStringLiteral("EXDATE;VALUE=DATE:") + dates.join(QLatin1Char(','))));
694 }
695
696 if (!recurrence.isEmpty()) {
697 data.insert(eventRecurrenceParam, recurrence);
698 }
699
700 SerializeDtFlags dtFlags;
701 if (event->allDay()) {
702 dtFlags |= SerializeDtFlag::AllDay;
703 }
704 if (!recurrence.isEmpty()) {
705 dtFlags |= SerializeDtFlag::HasRecurrence;
706 }
707
708 data.insert(eventStartPram, serializeDt(event, event->dtStart(), dtFlags));
709 data.insert(eventEndParam, serializeDt(event, event->dtEnd(), dtFlags | SerializeDtFlag::IsDtEnd));
710
711 if (event->hasRecurrenceId()) {
712 data.insert(eventOrganizerParam, serializeDt(event, event->recurrenceId(), dtFlags));
713 data.insert(eventRecurringEventIdParam, event->id());
714 }
715
716 if (event->transparency() == Event::Transparent) {
717 data.insert(eventTransparencyParam, transparentTransparency);
718 } else {
719 data.insert(eventTransparencyParam, opaqueTransparency);
720 }
721
722 QVariantList atts;
723 const auto attendees = event->attendees();
724 for (const auto &attee : attendees) {
725 QVariantMap att{{attendeeDisplayNameParam, attee.name()}, {attendeeEmailParam, attee.email()}};
726
727 if (attee.status() == KCalendarCore::Attendee::Accepted) {
728 att.insert(attendeeResponseStatusParam, acceptedStatus);
729 } else if (attee.status() == KCalendarCore::Attendee::Declined) {
730 att.insert(attendeeResponseStatusParam, declinedStatus);
731 } else if (attee.status() == KCalendarCore::Attendee::Tentative) {
732 att.insert(attendeeResponseStatusParam, tentativeStatus);
733 } else {
734 att.insert(attendeeResponseStatusParam, needsActionStatus);
735 }
736
737 if (attee.role() == KCalendarCore::Attendee::OptParticipant) {
738 att.insert(attendeeOptionalParam, true);
739 }
740 if (!attee.uid().isEmpty()) {
741 att.insert(idParam, attee.uid());
742 }
743 atts.append(att);
744 }
745
746 if (!atts.isEmpty()) {
747 data.insert(eventAttendeesParam, atts);
748
749 /* According to RFC, event without attendees should not have
750 * any organizer. */
751 const auto organizer = event->organizer();
752 if (!organizer.isEmpty()) {
753 data.insert(eventOrganizerParam, QVariantMap{{organizerDisplayNameParam, organizer.fullName()}, {organizerEmailParam, organizer.email()}});
754 }
755 }
756
757 QVariantList overrides;
758 const auto alarms = event->alarms();
759 for (const auto &alarm : alarms) {
760 QVariantMap reminderOverride;
761 if (alarm->type() == KCalendarCore::Alarm::Display) {
762 reminderOverride.insert(reminderMethodParam, popupMethod);
763 } else if (alarm->type() == KCalendarCore::Alarm::Email) {
764 reminderOverride.insert(reminderMethodParam, emailMethod);
765 } else {
766 continue;
767 }
768 reminderOverride.insert(reminderMinutesParam, (int)(alarm->startOffset().asSeconds() / -60));
769
770 overrides.push_back(reminderOverride);
771 }
772
773 data.insert(eventRemindersParam, QVariantMap{{reminderUseDefaultParam, false}, {reminderOverridesParam, overrides}});
774
775 if (!event->categories().isEmpty()) {
776 data.insert(eventExtendedPropertiesParam, QVariantMap{{propertySharedParam, QVariantMap{{categoriesProperty, event->categoriesStr()}}}});
777 }
778
779 // eventType not allowed in update, only in create
780 if (!data.contains(idParam)) {
781 data.insert(eventTypeParam, eventTypeToString(event->eventType()));
782 }
783
784 /* TODO: Implement support for additional features:
785 * https://developers.google.com/calendar/api/v3/reference/events/insert
786 */
787
788 const auto document = QJsonDocument::fromVariant(data);
789 return document.toJson(QJsonDocument::Compact);
790}
791
792ObjectsList parseEventJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
793{
794 const auto document = QJsonDocument::fromJson(jsonFeed);
795 const auto data = document.toVariant().toMap();
796
797 QString timezone;
798 if (data.value(kindParam) == eventsKind) {
799 if (data.contains(nextPageTokenParam)) {
800 QString calendarId = feedData.requestUrl.toString().remove(QStringLiteral("https://www.googleapis.com/calendar/v3/calendars/"));
801 calendarId = calendarId.left(calendarId.indexOf(QLatin1Char('/')));
802 feedData.nextPageUrl = feedData.requestUrl;
803 // replace the old pageToken with a new one
804 QUrlQuery query(feedData.nextPageUrl);
805 query.removeQueryItem(pageTokenParam);
806 query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
807 feedData.nextPageUrl.setQuery(query);
808 }
809 if (data.contains(timeZoneParam)) {
810 // This should always be in Olson format
811 timezone = data.value(timeZoneParam).toString();
812 }
813 if (data.contains(nextSyncTokenParam)) {
814 feedData.syncToken = data[nextSyncTokenParam].toString();
815 }
816 } else {
817 return {};
818 }
819
820 ObjectsList list;
821 const auto items = data.value(itemsParam).toList();
822 list.reserve(items.size());
823 for (const auto &i : items) {
824 list.push_back(Private::JSONToEvent(i.toMap(), timezone));
825 }
826
827 return list;
828}
829
830QString eventTypeToString(Event::EventType eventType)
831{
832 switch (eventType) {
833 case Event::EventType::Default:
834 return QStringLiteral("default");
835 case Event::EventType::FocusTime:
836 return QStringLiteral("focusTime");
837 case Event::EventType::OutOfOffice:
838 return QStringLiteral("outOfOffice");
839 case Event::EventType::WorkingLocation:
840 return QStringLiteral("workingLocation");
841 }
842
843 Q_UNREACHABLE();
844 return {};
845}
846
847Event::EventType eventTypeFromString(const QString &eventType)
848{
849 const auto eventTypeLc = eventType.toLower();
850 if (eventTypeLc == u"default") {
851 return Event::EventType::Default;
852 }
853 if (eventTypeLc == u"outofoffice") {
854 return Event::EventType::OutOfOffice;
855 }
856 if (eventTypeLc == u"focustime") {
857 return Event::EventType::FocusTime;
858 }
859 if (eventTypeLc == u"workinglocation") {
860 return Event::EventType::WorkingLocation;
861 }
862
863 return Event::EventType::Default;
864}
865
866/******************************** PRIVATE ***************************************/
867
868KCalendarCore::DateList Private::parseRDate(const QString &rule)
869{
871 QTimeZone tz;
872 QStringView value;
873 const auto left = QStringView(rule).left(rule.indexOf(QLatin1Char(':')));
874
875 const auto params = left.split(QLatin1Char(';'));
876 for (const auto &param : params) {
877 if (param.startsWith(QLatin1StringView("VALUE"))) {
878 value = param.mid(param.indexOf(QLatin1Char('=')) + 1);
879 } else if (param.startsWith(QLatin1StringView("TZID"))) {
880 auto _name = param.mid(param.indexOf(QLatin1Char('=')) + 1);
881 tz = QTimeZone(_name.toUtf8());
882 }
883 }
884 const auto datesStr = QStringView(rule).mid(rule.lastIndexOf(QLatin1Char(':')) + 1);
885 const auto dates = datesStr.split(QLatin1Char(','));
886 for (const auto &date : dates) {
887 QDate dt;
888
889 if (value == QLatin1StringView("DATE")) {
890 dt = QDate::fromString(date.toString(), QStringLiteral("yyyyMMdd"));
891 } else if (value == QLatin1StringView("PERIOD")) {
892 const auto start = date.left(date.indexOf(QLatin1Char('/')));
893 QDateTime kdt = Utils::rfc3339DateFromString(start.toString());
894 if (tz.isValid()) {
895 kdt.setTimeZone(tz);
896 }
897
898 dt = kdt.date();
899 } else {
900 QDateTime kdt = Utils::rfc3339DateFromString(date.toString());
901 if (tz.isValid()) {
902 kdt.setTimeZone(tz);
903 }
904
905 dt = kdt.date();
906 }
907
908 list.push_back(dt);
909 }
910
911 return list;
912}
913
914namespace
915{
916
917/* Based on "Time Zone to CdoTimeZoneId Map"
918 * https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2007/aa563018(v=exchg.80)
919 *
920 * The mapping is not exact, since the CdoTimeZoneId usually refers to a
921 * region of multiple countries, so I always picked one of the countries
922 * in the specified region and used it's TZID.
923 */
924static const std::map<int, QLatin1StringView> MSCDOTZIDTable = {
925 {0, QLatin1StringView("UTC")},
926 {1, QLatin1StringView("Europe/London")}, /* GMT Greenwich Mean Time; Dublin, Edinburgh, London */
927 /* Seriously? *sigh* Let's handle these two in checkAndConvertCDOTZID() */
928 //{2, QLatin1StringView("Europe/Lisbon")}, /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
929 //{2, QLatin1StringView("Europe/Sarajevo")}, /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
930 {3, QLatin1StringView("Europe/Paris")}, /* GMT+01:00 Paris, Madrid, Brussels, Copenhagen */
931 {4, QLatin1StringView("Europe/Berlin")}, /* GMT+01:00 Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
932 {5, QLatin1StringView("Europe/Bucharest")}, /* GMT+02:00 Bucharest */
933 {6, QLatin1StringView("Europe/Prague")}, /* GMT+01:00 Prague, Central Europe */
934 {7, QLatin1StringView("Europe/Athens")}, /* GMT+02:00 Athens, Istanbul, Minsk */
935 {8, QLatin1StringView("America/Brazil")}, /* GMT-03:00 Brasilia */
936 {9, QLatin1StringView("America/Halifax")}, /* GMT-04:00 Atlantic time (Canada) */
937 {10, QLatin1StringView("America/New_York")}, /* GMT-05:00 Eastern Time (US & Canada) */
938 {11, QLatin1StringView("America/Chicago")}, /* GMT-06:00 Central Time (US & Canada) */
939 {12, QLatin1StringView("America/Denver")}, /* GMT-07:00 Mountain Time (US & Canada) */
940 {13, QLatin1StringView("America/Los_Angeles")}, /* GMT-08:00 Pacific Time (US & Canada); Tijuana */
941 {14, QLatin1StringView("America/Anchorage")}, /* GMT-09:00 Alaska */
942 {15, QLatin1StringView("Pacific/Honolulu")}, /* GMT-10:00 Hawaii */
943 {16, QLatin1StringView("Pacific/Apia")}, /* GMT-11:00 Midway Island, Samoa */
944 {17, QLatin1StringView("Pacific/Auckland")}, /* GMT+12:00 Auckland, Wellington */
945 {18, QLatin1StringView("Australia/Brisbane")}, /* GMT+10:00 Brisbane, East Australia */
946 {19, QLatin1StringView("Australia/Adelaide")}, /* GMT+09:30 Adelaide, Central Australia */
947 {20, QLatin1StringView("Asia/Tokyo")}, /* GMT+09:00 Osaka, Sapporo, Tokyo */
948 {21, QLatin1StringView("Asia/Singapore")}, /* GMT+08:00 Kuala Lumpur, Singapore */
949 {22, QLatin1StringView("Asia/Bangkok")}, /* GMT+07:00 Bangkok, Hanoi, Jakarta */
950 {23, QLatin1StringView("Asia/Calcutta")}, /* GMT+05:30 Kolkata, Chennai, Mumbai, New Delhi, India Standard Time */
951 {24, QLatin1StringView("Asia/Dubai")}, /* GMT+04:00 Abu Dhabi, Muscat */
952 {25, QLatin1StringView("Asia/Tehran")}, /* GMT+03:30 Tehran */
953 {26, QLatin1StringView("Asia/Baghdad")}, /* GMT+03:00 Baghdad */
954 {27, QLatin1StringView("Asia/Jerusalem")}, /* GMT+02:00 Israel, Jerusalem Standard Time */
955 {28, QLatin1StringView("America/St_Johns")}, /* GMT-03:30 Newfoundland */
956 {29, QLatin1StringView("Atlantic/Portugal")}, /* GMT-01:00 Azores */
957 {30, QLatin1StringView("America/Noronha")}, /* GMT-02:00 Mid-Atlantic */
958 {31, QLatin1StringView("Africa/Monrovia")}, /* GMT Casablanca, Monrovia */
959 {32, QLatin1StringView("America/Argentina/Buenos_Aires")}, /* GMT-03:00 Buenos Aires, Georgetown */
960 {33, QLatin1StringView("America/La_Paz")}, /* GMT-04:00 Caracas, La Paz */
961 {34, QLatin1StringView("America/New_York")}, /* GMT-05:00 Indiana (East) */
962 {35, QLatin1StringView("America/Bogota")}, /* GMT-05:00 Bogota, Lima, Quito */
963 {36, QLatin1StringView("America/Winnipeg")}, /* GMT-06:00 Saskatchewan */
964 {37, QLatin1StringView("America/Mexico_City")}, /* GMT-06:00 Mexico City, Tegucigalpa */
965 {38, QLatin1StringView("America/Phoenix")}, /* GMT-07:00 Arizona */
966 {39, QLatin1StringView("Pacific/Kwajalein")}, /* GMT-12:00 Eniwetok, Kwajalein, Dateline Time */
967 {40, QLatin1StringView("Pacific/Fiji")}, /* GMT+12:00 Fušál, Kamchatka, Mashall Is. */
968 {41, QLatin1StringView("Pacific/Noumea")}, /* GMT+11:00 Magadan, Solomon Is., New Caledonia */
969 {42, QLatin1StringView("Australia/Hobart")}, /* GMT+10:00 Hobart, Tasmania */
970 {43, QLatin1StringView("Pacific/Guam")}, /* GMT+10:00 Guam, Port Moresby */
971 {44, QLatin1StringView("Australia/Darwin")}, /* GMT+09:30 Darwin */
972 {45, QLatin1StringView("Asia/Shanghai")}, /* GMT+08:00 Beijing, Chongqing, Hong Kong SAR, Urumqi */
973 {46, QLatin1StringView("Asia/Omsk")}, /* GMT+06:00 Almaty, Novosibirsk, North Central Asia */
974 {47, QLatin1StringView("Asia/Karachi")}, /* GMT+05:00 Islamabad, Karachi, Tashkent */
975 {48, QLatin1StringView("Asia/Kabul")}, /* GMT+04:30 Kabul */
976 {49, QLatin1StringView("Africa/Cairo")}, /* GMT+02:00 Cairo */
977 {50, QLatin1StringView("Africa/Harare")}, /* GMT+02:00 Harare, Pretoria */
978 {51, QLatin1StringView("Europe/Moscow")}, /* GMT+03:00 Moscow, St. Petersburg, Volgograd */
979 {53, QLatin1StringView("Atlantic/Cape_Verde")}, /* GMT-01:00 Cape Verde Is. */
980 {54, QLatin1StringView("Asia/Tbilisi")}, /* GMT+04:00 Baku, Tbilisi, Yerevan */
981 {55, QLatin1StringView("America/Tegucigalpa")}, /* GMT-06:00 Central America */
982 {56, QLatin1StringView("Africa/Nairobi")}, /* GMT+03:00 East Africa, Nairobi */
983 {58, QLatin1StringView("Asia/Yekaterinburg")}, /* GMT+05:00 Ekaterinburg */
984 {59, QLatin1StringView("Europe/Helsinki")}, /* GMT+02:00 Helsinki, Riga, Tallinn */
985 {60, QLatin1StringView("America/Greenland")}, /* GMT-03:00 Greenland */
986 {61, QLatin1StringView("Asia/Rangoon")}, /* GMT+06:30 Yangon (Rangoon) */
987 {62, QLatin1StringView("Asia/Katmandu")}, /* GMT+05:45 Kathmandu, Nepal */
988 {63, QLatin1StringView("Asia/Irkutsk")}, /* GMT+08:00 Irkutsk, Ulaan Bataar */
989 {64, QLatin1StringView("Asia/Krasnoyarsk")}, /* GMT+07:00 Krasnoyarsk */
990 {65, QLatin1StringView("America/Santiago")}, /* GMT-04:00 Santiago */
991 {66, QLatin1StringView("Asia/Colombo")}, /* GMT+06:00 Sri Jayawardenepura, Sri Lanka */
992 {67, QLatin1StringView("Pacific/Tongatapu")}, /* GMT+13:00 Nuku'alofa, Tonga */
993 {68, QLatin1StringView("Asia/Vladivostok")}, /* GMT+10:00 Vladivostok */
994 {69, QLatin1StringView("Africa/Bangui")}, /* GMT+01:00 West Central Africa */
995 {70, QLatin1StringView("Asia/Yakutsk")}, /* GMT+09:00 Yakutsk */
996 {71, QLatin1StringView("Asia/Dhaka")}, /* GMT+06:00 Astana, Dhaka */
997 {72, QLatin1StringView("Asia/Seoul")}, /* GMT+09:00 Seoul, Korea Standard time */
998 {73, QLatin1StringView("Australia/Perth")}, /* GMT+08:00 Perth, Western Australia */
999 {74, QLatin1StringView("Asia/Kuwait")}, /* GMT+03:00 Arab, Kuwait, Riyadh */
1000 {75, QLatin1StringView("Asia/Taipei")}, /* GMT+08:00 Taipei */
1001 {76, QLatin1StringView("Australia/Sydney")} /* GMT+10:00 Canberra, Melbourne, Sydney */
1002};
1003
1004/* Based on "Microsoft Time Zone Index Values"
1005 * https://support.microsoft.com/en-gb/help/973627/microsoft-time-zone-index-values
1006 *
1007 * The mapping is not exact, since the TZID usually refers to a
1008 * region of multiple countries, so I always picked one of the countries
1009 * in the specified region and used it's TZID.
1010 *
1011 * The Olson timezones are taken from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
1012 *
1013 * Note: using std::map, because it allows heterogeneous lookup, i.e. I can lookup the QLatin1StringView
1014 * keys by using QString value, which is not possible with Qt containers.
1015 */
1016static const std::map<QLatin1StringView, QLatin1StringView, std::less<>> MSSTTZTable = {
1017 {QLatin1StringView("Dateline Standard Time"), QLatin1StringView("Pacific/Kwajalein")}, /* (GMT-12:00) International Date Line West */
1018 {QLatin1StringView("Samoa Standard Time"), QLatin1StringView("Pacific/Apia")}, /* (GMT-11:00) Midway Island, Samoa */
1019 {QLatin1StringView("Hawaiian Standard Time"), QLatin1StringView("Pacific/Honolulu")}, /* (GMT-10:00) Hawaii */
1020 {QLatin1StringView("Alaskan Standard Time"), QLatin1StringView("America/Anchorage")}, /* (GMT-09:00) Alaska */
1021 {QLatin1StringView("Pacific Standard Time"), QLatin1StringView("America/Los_Angeles")}, /* (GMT-08:00) Pacific Time (US and Canada); Tijuana */
1022 {QLatin1StringView("Mountain Standard Time"), QLatin1StringView("America/Denver")}, /* (GMT-07:00) Mountain Time (US and Canada) */
1023 {QLatin1StringView("Mexico Standard Time 2"), QLatin1StringView("America/Chihuahua")}, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */
1024 {QLatin1StringView("U.S. Mountain Standard Time"), QLatin1StringView("America/Phoenix")}, /* (GMT-07:00) Arizona */
1025 {QLatin1StringView("Central Standard Time"), QLatin1StringView("America/Chicago")}, /* (GMT-06:00) Central Time (US and Canada */
1026 {QLatin1StringView("Canada Central Standard Time"), QLatin1StringView("America/Winnipeg")}, /* (GMT-06:00) Saskatchewan */
1027 {QLatin1StringView("Mexico Standard Time"), QLatin1StringView("America/Mexico_City")}, /* (GMT-06:00) Guadalajara, Mexico City, Monterrey */
1028 {QLatin1StringView("Central America Standard Time"), QLatin1StringView("America/Chicago")}, /* (GMT-06:00) Central America */
1029 {QLatin1StringView("Eastern Standard Time"), QLatin1StringView("America/New_York")}, /* (GMT-05:00) Eastern Time (US and Canada) */
1030 {QLatin1StringView("U.S. Eastern Standard Time"), QLatin1StringView("America/New_York")}, /* (GMT-05:00) Indiana (East) */
1031 {QLatin1StringView("S.A. Pacific Standard Time"), QLatin1StringView("America/Bogota")}, /* (GMT-05:00) Bogota, Lima, Quito */
1032 {QLatin1StringView("Atlantic Standard Time"), QLatin1StringView("America/Halifax")}, /* (GMT-04:00) Atlantic Time (Canada) */
1033 {QLatin1StringView("S.A. Western Standard Time"), QLatin1StringView("America/La_Paz")}, /* (GMT-04:00) Caracas, La Paz */
1034 {QLatin1StringView("Pacific S.A. Standard Time"), QLatin1StringView("America/Santiago")}, /* (GMT-04:00) Santiago */
1035 {QLatin1StringView("Newfoundland and Labrador Standard Time"), QLatin1StringView("America/St_Johns")}, /* (GMT-03:30) Newfoundland and Labrador */
1036 {QLatin1StringView("E. South America Standard Time"), QLatin1StringView("America/Brazil")}, /* (GMT-03:00) Brasilia */
1037 {QLatin1StringView("S.A. Eastern Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires, Georgetown */
1038 {QLatin1StringView("Greenland Standard Time"), QLatin1StringView("America/Greenland")}, /* (GMT-03:00) Greenland */
1039 {QLatin1StringView("Mid-Atlantic Standard Time"), QLatin1StringView("America/Noronha")}, /* (GMT-02:00) Mid-Atlantic */
1040 {QLatin1StringView("Azores Standard Time"), QLatin1StringView("Atlantic/Portugal")}, /* (GMT-01:00) Azores */
1041 {QLatin1StringView("Cape Verde Standard Time"), QLatin1StringView("Atlantic/Cape_Verde")}, /* (GMT-01:00) Cape Verde Islands */
1042 {QLatin1StringView("GMT Standard Time"), QLatin1StringView("Europe/London")}, /* (GMT) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
1043 {QLatin1StringView("Greenwich Standard Time"), QLatin1StringView("Africa/Casablanca")}, /* (GMT) Casablanca, Monrovia */
1044 {QLatin1StringView("Central Europe Standard Time"), QLatin1StringView("Europe/Prague")}, /* (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague */
1045 {QLatin1StringView("Central European Standard Time"), QLatin1StringView("Europe/Sarajevo")}, /* (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb */
1046 {QLatin1StringView("Romance Standard Time"), QLatin1StringView("Europe/Brussels")}, /* (GMT+01:00) Brussels, Copenhagen, Madrid, Paris */
1047 {QLatin1StringView("W. Europe Standard Time"), QLatin1StringView("Europe/Amsterdam")}, /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
1048 {QLatin1StringView("W. Central Africa Standard Time"), QLatin1StringView("Africa/Bangui")}, /* (GMT+01:00) West Central Africa */
1049 {QLatin1StringView("E. Europe Standard Time"), QLatin1StringView("Europe/Bucharest")}, /* (GMT+02:00) Bucharest */
1050 {QLatin1StringView("Egypt Standard Time"), QLatin1StringView("Africa/Cairo")}, /* (GMT+02:00) Cairo */
1051 {QLatin1StringView("FLE Standard Time"), QLatin1StringView("Europe/Helsinki")}, /* (GMT+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius */
1052 {QLatin1StringView("GTB Standard Time"), QLatin1StringView("Europe/Athens")}, /* (GMT+02:00) Athens, Istanbul, Minsk */
1053 {QLatin1StringView("Israel Standard Time"), QLatin1StringView("Europe/Athens")}, /* (GMT+02:00) Jerusalem */
1054 {QLatin1StringView("South Africa Standard Time"), QLatin1StringView("Africa/Harare")}, /* (GMT+02:00) Harare, Pretoria */
1055 {QLatin1StringView("Russian Standard Time"), QLatin1StringView("Europe/Moscow")}, /* (GMT+03:00) Moscow, St. Petersburg, Volgograd */
1056 {QLatin1StringView("Arab Standard Time"), QLatin1StringView("Asia/Kuwait")}, /* (GMT+03:00) Kuwait, Riyadh */
1057 {QLatin1StringView("E. Africa Standard Time"), QLatin1StringView("Africa/Nairobi")}, /* (GMT+03:00) Nairobi */
1058 {QLatin1StringView("Arabic Standard Time"), QLatin1StringView("Asia/Baghdad")}, /* (GMT+03:00) Baghdad */
1059 {QLatin1StringView("Iran Standard Time"), QLatin1StringView("Asia/Tehran")}, /* (GMT+03:30) Tehran */
1060 {QLatin1StringView("Arabian Standard Time"), QLatin1StringView("Asia/Dubai")}, /* (GMT+04:00) Abu Dhabi, Muscat */
1061 {QLatin1StringView("Caucasus Standard Time"), QLatin1StringView("Asia/Tbilisi")}, /* (GMT+04:00) Baku, Tbilisi, Yerevan */
1062 {QLatin1StringView("Transitional Islamic State of Afghanistan Standard Time"), QLatin1StringView("Asia/Kabul")}, /* (GMT+04:30) Kabul */
1063 {QLatin1StringView("Ekaterinburg Standard Time"), QLatin1StringView("Asia/Yekaterinburg")}, /* (GMT+05:00) Ekaterinburg */
1064 {QLatin1StringView("West Asia Standard Time"), QLatin1StringView("Asia/Karachi")}, /* (GMT+05:00) Islamabad, Karachi, Tashkent */
1065 {QLatin1StringView("India Standard Time"), QLatin1StringView("Asia/Calcutta")}, /* (GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi */
1066 {QLatin1StringView("Nepal Standard Time"), QLatin1StringView("Asia/Calcutta")}, /* (GMT+05:45) Kathmandu */
1067 {QLatin1StringView("Central Asia Standard Time"), QLatin1StringView("Asia/Dhaka")}, /* (GMT+06:00) Astana, Dhaka */
1068 {QLatin1StringView("Sri Lanka Standard Time"), QLatin1StringView("Asia/Colombo")}, /* (GMT+06:00) Sri Jayawardenepura */
1069 {QLatin1StringView("N. Central Asia Standard Time"), QLatin1StringView("Asia/Omsk")}, /* (GMT+06:00) Almaty, Novosibirsk */
1070 {QLatin1StringView("Myanmar Standard Time"), QLatin1StringView("Asia/Rangoon")}, /* (GMT+06:30) Yangon Rangoon */
1071 {QLatin1StringView("S.E. Asia Standard Time"), QLatin1StringView("Asia/Bangkok")}, /* (GMT+07:00) Bangkok, Hanoi, Jakarta */
1072 {QLatin1StringView("North Asia Standard Time"), QLatin1StringView("Asia/Krasnoyarsk")}, /* (GMT+07:00) Krasnoyarsk */
1073 {QLatin1StringView("China Standard Time"), QLatin1StringView("Asia/Shanghai")}, /* (GMT+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi */
1074 {QLatin1StringView("Singapore Standard Time"), QLatin1StringView("Asia/Singapore")}, /* (GMT+08:00) Kuala Lumpur, Singapore */
1075 {QLatin1StringView("Taipei Standard Time"), QLatin1StringView("Asia/Taipei")}, /* (GMT+08:00) Taipei */
1076 {QLatin1StringView("W. Australia Standard Time"), QLatin1StringView("Australia/Perth")}, /* (GMT+08:00) Perth */
1077 {QLatin1StringView("North Asia East Standard Time"), QLatin1StringView("Asia/Irkutsk")}, /* (GMT+08:00) Irkutsk, Ulaanbaatar */
1078 {QLatin1StringView("Korea Standard Time"), QLatin1StringView("Asia/Seoul")}, /* (GMT+09:00) Seoul */
1079 {QLatin1StringView("Tokyo Standard Time"), QLatin1StringView("Asia/Tokyo")}, /* (GMT+09:00) Osaka, Sapporo, Tokyo */
1080 {QLatin1StringView("Yakutsk Standard Time"), QLatin1StringView("Asia/Yakutsk")}, /* (GMT+09:00) Yakutsk */
1081 {QLatin1StringView("A.U.S. Central Standard Time"), QLatin1StringView("Australia/Darwin")}, /* (GMT+09:30) Darwin */
1082 {QLatin1StringView("Cen. Australia Standard Time"), QLatin1StringView("Australia/Adelaide")}, /* (GMT+09:30) Adelaide */
1083 {QLatin1StringView("A.U.S. Eastern Standard Time"), QLatin1StringView("Australia/Sydney")}, /* (GMT+10:00) Canberra, Melbourne, Sydney */
1084 {QLatin1StringView("E. Australia Standard Time"), QLatin1StringView("Australia/Brisbane")}, /* (GMT+10:00) Brisbane */
1085 {QLatin1StringView("Tasmania Standard Time"), QLatin1StringView("Australia/Hobart")}, /* (GMT+10:00) Hobart */
1086 {QLatin1StringView("Vladivostok Standard Time"), QLatin1StringView("Asia/Vladivostok")}, /* (GMT+10:00) Vladivostok */
1087 {QLatin1StringView("West Pacific Standard Time"), QLatin1StringView("Pacific/Guam")}, /* (GMT+10:00) Guam, Port Moresby */
1088 {QLatin1StringView("Central Pacific Standard Time"), QLatin1StringView("Pacific/Noumea")}, /* (GMT+11:00) Magadan, Solomon Islands, New Caledonia */
1089 {QLatin1StringView("Fiji Islands Standard Time"), QLatin1StringView("Pacific/Fiji")}, /* (GMT+12:00) Fiji Islands, Kamchatka, Marshall Islands */
1090 {QLatin1StringView("New Zealand Standard Time"), QLatin1StringView("Pacific/Auckland")}, /* (GMT+12:00) Auckland, Wellington */
1091 {QLatin1StringView("Tonga Standard Time"), QLatin1StringView("Pacific/Tongatapu")}, /* (GMT+13:00) Nuku'alofa */
1092 {QLatin1StringView("Azerbaijan Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires */
1093 {QLatin1StringView("Middle East Standard Time"), QLatin1StringView("Asia/Beirut")}, /* (GMT+02:00) Beirut */
1094 {QLatin1StringView("Jordan Standard Time"), QLatin1StringView("Asia/Amman")}, /* (GMT+02:00) Amman */
1095 {QLatin1StringView("Central Standard Time (Mexico)"), QLatin1StringView("America/Mexico_City")}, /* (GMT-06:00) Guadalajara, Mexico City, Monterrey - New */
1096 {QLatin1StringView("Mountain Standard Time (Mexico)"), QLatin1StringView("America/Ojinaga")}, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan - New */
1097 {QLatin1StringView("Pacific Standard Time (Mexico)"), QLatin1StringView("America/Tijuana")}, /* (GMT-08:00) Tijuana, Baja California */
1098 {QLatin1StringView("Namibia Standard Time"), QLatin1StringView("Africa/Windhoek")}, /* (GMT+02:00) Windhoek */
1099 {QLatin1StringView("Georgian Standard Time"), QLatin1StringView("Asia/Tbilisi")}, /* (GMT+03:00) Tbilisi */
1100 {QLatin1StringView("Central Brazilian Standard Time"), QLatin1StringView("America/Manaus")}, /*(GMT-04:00) Manaus */
1101 {QLatin1StringView("Montevideo Standard Time"), QLatin1StringView("America/Montevideo")}, /* (GMT-03:00) Montevideo */
1102 {QLatin1StringView("Armenian Standard Time"), QLatin1StringView("Asia/Yerevan")}, /* (GMT+04:00) Yerevan */
1103 {QLatin1StringView("Venezuela Standard Time"), QLatin1StringView("America/Caracas")}, /* (GMT-04:30) Caracas */
1104 {QLatin1StringView("Argentina Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires */
1105 {QLatin1StringView("Morocco Standard Time"), QLatin1StringView("Africa/Casablanca")}, /* (GMT) Casablanca */
1106 {QLatin1StringView("Pakistan Standard Time"), QLatin1StringView("Asia/Karachi")}, /* (GMT+05:00) Islamabad, Karachi */
1107 {QLatin1StringView("Mauritius Standard Time"), QLatin1StringView("Indian/Mauritius")}, /* (GMT+04:00) Port Louis */
1108 {QLatin1StringView("UTC"), QLatin1StringView("UTC")}, /* (GMT) Coordinated Universal Time */
1109 {QLatin1StringView("Paraguay Standard Time"), QLatin1StringView("America/Asuncion")}, /* (GMT-04:00) Asuncion */
1110 {QLatin1StringView("Kamchatka Standard Time"), QLatin1StringView("Asia/Kamchatka")}, /* (GMT+12:00) Petropavlovsk-Kamchatsky */
1111};
1112} // namespace
1113
1114QString Private::checkAndConverCDOTZID(const QString &tzid, const EventPtr &event)
1115{
1116 /* Try to match the @tzid to any valid timezone we know. */
1117 QTimeZone tz(tzid.toLatin1());
1118 if (tz.isValid()) {
1119 /* Yay, @tzid is a valid TZID in Olson format */
1120 return tzid;
1121 }
1122
1123 /* Damn, no match. Parse the iCal and try to find X-MICROSOFT-CDO-TZID
1124 * property that we can match against the MSCDOTZIDTable */
1125 KCalendarCore::ICalFormat format;
1126 /* Use a copy of @event, otherwise it would be deleted when ptr is destroyed */
1127 KCalendarCore::Incidence::Ptr incidence = event.dynamicCast<KCalendarCore::Incidence>();
1128 const QString vcard = format.toICalString(incidence);
1129 const QStringList properties = vcard.split(QLatin1Char('\n'));
1130 int CDOId = -1;
1131 for (const QString &property : properties) {
1132 if (property.startsWith(u"X-MICROSOFT-CDO-TZID")) {
1133 QStringList parsed = property.split(QLatin1Char(':'));
1134 if (parsed.length() != 2) {
1135 break;
1136 }
1137
1138 CDOId = parsed.at(1).toInt();
1139 break;
1140 }
1141 }
1142
1143 /* Wheeee, we have X-MICROSOFT-CDO-TZID, try to map it to Olson format */
1144 if (CDOId > -1) {
1145 /* *sigh* Some expert in MS assigned the same ID to two different timezones... */
1146 if (CDOId == 2) {
1147 /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
1148 if (tzid.contains(QLatin1StringView("Dublin")) || tzid.contains(QLatin1StringView("Edinburgh")) || tzid.contains(QLatin1StringView("Lisbon"))
1149 || tzid.contains(QLatin1StringView("London"))) {
1150 return QStringLiteral("Europe/London");
1151 }
1152
1153 /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
1154 else if (tzid.contains(QLatin1StringView("Sarajevo")) || tzid.contains(QLatin1StringView("Skopje")) || tzid.contains(QLatin1StringView("Sofija"))
1155 || tzid.contains(QLatin1StringView("Vilnius")) || tzid.contains(QLatin1StringView("Warsaw")) || tzid.contains(QLatin1StringView("Zagreb"))) {
1156 return QStringLiteral("Europe/Sarajevo");
1157 }
1158 }
1159
1160 const auto it = MSCDOTZIDTable.find(CDOId);
1161 if (it != MSCDOTZIDTable.cend()) {
1162 return it->second;
1163 }
1164 }
1165
1166 /* We failed to map to X-MICROSOFT-CDO-TZID. Let's try mapping the TZID
1167 * onto the Microsoft Standard Time Zones */
1168 const auto it = MSSTTZTable.find(tzid);
1169 if (it != MSSTTZTable.cend()) {
1170 return it->second;
1171 }
1172
1173 /* Fail/ Just return the original TZID and hope Google will accept it
1174 * (though we know it won't) */
1175 return tzid;
1176}
1177
1178} // namespace CalendarService
1179
1180} // namespace KGAPI2
QString toString(const Calendar::Ptr &calendar) override
QString toICalString(const Incidence::Ptr &incidence)
bool fromString(const Calendar::Ptr &calendar, const QString &string)
QSharedPointer< Incidence > Ptr
QString email() const
void setEmail(const QString &email)
QString fullName() const
void setName(const QString &name)
bool isEmpty() const
An object that represents a Google calendar.
Definition calendar.h:28
Represents a single event from Google Calendar.
Structure to store additional information about a feed.
Definition types.h:24
QString syncToken
Sync token that can be used for incremental updates by some of the services.
Definition types.h:42
QUrl requestUrl
Original URL of the request.
Definition types.h:39
QUrl nextPageUrl
Link to next page of feed.
Definition types.h:38
Q_SCRIPTABLE Q_NOREPLY void start()
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QList< QDate > DateList
Additional methods for implementing support for Google Calendar service.
QByteArray eventToJSON(const EventPtr &event, EventSerializeFlags flags)
Serializes Event into JSON.
QUrl updateCalendarUrl(const QString &calendarID)
Returns URL for updating existing calendar.
QUrl fetchCalendarsUrl()
Returns URL for fetching calendars list.
QUrl removeCalendarUrl(const QString &calendarID)
Returns URL for removing an existing calendar.
QUrl updateEventUrl(const QString &calendarID, const QString &eventID, SendUpdatesPolicy updatePolicy)
Returns URL for updating a single event.
QUrl createEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
Returns URL creating new events.
ObjectsList parseEventJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
Parses JSON feed into list of Events.
QUrl importEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
Returns URL importing private copies of existing events.
QUrl createCalendarUrl()
Returns URL for creating a new calendar.
QUrl moveEventUrl(const QString &sourceCalendar, const QString &destCalendar, const QString &eventID)
Returns URL for moving event between calendars.
QByteArray calendarToJSON(const CalendarPtr &calendar)
Serializes calendar into JSON.
QUrl fetchCalendarUrl(const QString &calendarID)
Returns URL for fetching single calendar.
CalendarPtr JSONToCalendar(const QByteArray &jsonData)
Parses calendar JSON data into Calendar object.
QNetworkRequest prepareRequest(const QUrl &url)
Preparse a QNetworkRequest for given URL.
QString eventTypeToString(Event::EventType eventType)
Converts event type enum value to string.
EventPtr JSONToEvent(const QByteArray &jsonData)
Parses event JSON into Event object.
ObjectsList parseCalendarJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
Parses JSON feed into list of Calendars.
QString APIVersion()
Supported API version.
QUrl freeBusyQueryUrl()
Returns URL for freebusy queries.
QUrl fetchEventsUrl(const QString &calendarID)
Returns URL for fetching all events from a specific calendar.
Event::EventType eventTypeFromString(const QString &eventType)
Converts event type string to enum value.
QUrl fetchEventUrl(const QString &calendarID, const QString &eventID)
Returns URL for fetching a single event from a specific calendar.
QUrl removeEventUrl(const QString &calendarID, const QString &eventID)
Returns URL for removing events.
A job to fetch a single map tile described by a StaticMapUrl.
Definition blog.h:16
SendUpdatesPolicy
Determines whether Google Calendar should send updates to participants.
Definition enums.h:22
@ ExternalOnly
Send updates to all attendees.
Definition enums.h:24
@ None
Only send updates to non-Google Calendar participants.
Definition enums.h:25
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem properties()
KTEXTEDITOR_EXPORT size_t qHash(KTextEditor::Cursor cursor, size_t seed=0) noexcept
QByteArray & insert(qsizetype i, QByteArrayView data)
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime addDays(qint64 ndays) const const
QDate date() const const
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
void setTimeZone(const QTimeZone &toZone)
QTimeZone timeZone() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonDocument fromVariant(const QVariant &variant)
QVariant toVariant() const const
const_reference at(qsizetype i) const const
void clear()
bool isEmpty() const const
qsizetype length() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
T value(qsizetype i) const const
void setRawHeader(const QByteArray &headerName, const QByteArray &headerValue)
QSharedPointer< T > create(Args &&... args)
T * data() const const
QSharedPointer< X > dynamicCast() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QByteArray toLatin1() const const
QString toLower() const const
QByteArray toUtf8() const const
QString join(QChar separator) const const
QStringView left(qsizetype length) const const
QStringView mid(qsizetype start, qsizetype length) const const
QByteArray id() const const
bool isValid() const const
QTimeZone utc()
QString fromPercentEncoding(const QByteArray &input)
void setPath(const QString &path, ParsingMode mode)
void setQuery(const QString &query, ParsingMode mode)
QString toString(FormattingOptions options) const const
QMap< QString, QVariant > toMap() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:48:07 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.