diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 326e3f2ba..fdba0fc1a 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -1,66 +1,67 @@ # Libkolabxml find_package(Libkolabxml 1.1 QUIET CONFIG) set_package_properties(Libkolabxml PROPERTIES DESCRIPTION "Kolabxml" URL "http://mirror.kolabsys.com/pub/releases" TYPE OPTIONAL PURPOSE "The Kolab XML Format Schema Definitions Library is required to build the Kolab Groupware Resource") include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/shared/singlefileresource/ ${CMAKE_CURRENT_BINARY_DIR}/shared/singlefileresource/ ${CMAKE_CURRENT_SOURCE_DIR}/folderarchivesettings/ ) # resource tests macro( akonadi_add_resourcetest _testname _script ) if ( ${EXECUTABLE_OUTPUT_PATH} ) set( _exepath ${EXECUTABLE_OUTPUT_PATH} ) else () set( _exepath ${kdepim-runtime_BINARY_DIR}/resourcetester ) endif () if (WIN32) set(_resourcetester ${_exepath}/resourcetester.bat) else () set(_resourcetester ${_exepath}/resourcetester) endif () if (UNIX) set(_resourcetester ${_resourcetester}.shell) endif () configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${_script} ${CMAKE_CURRENT_BINARY_DIR}/${_script} COPYONLY) if (KDEPIM_RUN_ISOLATED_TESTS) add_test( NAME akonadi-mysql-db-${_testname} COMMAND akonaditest -c ${kdepim-runtime_SOURCE_DIR}/resourcetester/tests/unittestenv/config-mysql-db.xml ${_resourcetester} -c ${CMAKE_CURRENT_BINARY_DIR}/${_script} ) endif () endmacro( akonadi_add_resourcetest ) add_subdirectory( akonotes ) if (TARGET KF5::AlarmCalendar) add_subdirectory( kalarm ) endif() add_subdirectory( contacts ) add_subdirectory( dav ) add_subdirectory( ical ) add_subdirectory( imap ) if (Libkolabxml_FOUND) find_package(Boost REQUIRED COMPONENTS thread system) add_subdirectory( kolab ) endif() #Disable it as it seems Facebook disabled the login and event API has been crippled #add_subdirectory( facebook ) add_subdirectory( maildir ) add_subdirectory( openxchange ) add_subdirectory( pop3 ) add_subdirectory( google ) +add_subdirectory( google-groupware ) add_subdirectory( shared ) add_subdirectory( birthdays ) add_subdirectory( mixedmaildir ) add_subdirectory( mbox ) add_subdirectory( vcarddir ) add_subdirectory( icaldir ) add_subdirectory( vcard ) add_subdirectory( folderarchivesettings ) add_subdirectory( tomboynotes ) add_subdirectory( ews ) diff --git a/resources/google-groupware/CMakeLists.txt b/resources/google-groupware/CMakeLists.txt new file mode 100644 index 000000000..2f189f08c --- /dev/null +++ b/resources/google-groupware/CMakeLists.txt @@ -0,0 +1,89 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_google_resource\") + +set(googleresource_SRCS + googleresource.cpp + googlesettings.cpp + googlesettingsdialog.cpp + defaultreminderattribute.cpp + googleresourcestate.cpp + generichandler.cpp + calendarhandler.cpp + taskhandler.cpp + contacthandler.cpp) + +ecm_qt_declare_logging_category(googleresource_SRCS + HEADER googleresource_debug.h + IDENTIFIER GOOGLE_LOG + CATEGORY_NAME org.kde.pim.google + DESCRIPTION "resource google (kdepim-runtime)" + EXPORT KDEPIMRUNTIME) +ecm_qt_declare_logging_category(googleresource_SRCS + HEADER googlecalendar_debug.h + IDENTIFIER GOOGLE_CALENDAR_LOG + CATEGORY_NAME org.kde.pim.google.calendar + DESCRIPTION "resource google calendar (kdepim-runtime)" + EXPORT KDEPIMRUNTIME) +ecm_qt_declare_logging_category(googleresource_SRCS + HEADER googletasks_debug.h + IDENTIFIER GOOGLE_TASKS_LOG + CATEGORY_NAME org.kde.pim.google.tasks + DESCRIPTION "resource google tasks (kdepim-runtime)" + EXPORT KDEPIMRUNTIME) +ecm_qt_declare_logging_category(googleresource_SRCS + HEADER googlecontacts_debug.h + IDENTIFIER GOOGLE_CONTACTS_LOG + CATEGORY_NAME org.kde.pim.google.contacts + DESCRIPTION "resource google contacts (kdepim-runtime)" + EXPORT KDEPIMRUNTIME) + +ki18n_wrap_ui(googleresource_SRCS googlesettingsdialog.ui) + +kconfig_add_kcfg_files(googleresource_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfgc) + +kcfg_generate_dbus_interface( + ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfg + org.kde.Akonadi.Google.Settings +) + +qt5_add_dbus_adaptor(googleresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Google.Settings.xml + ${CMAKE_CURRENT_SOURCE_DIR}/googlesettings.h GoogleSettings +) + +add_executable(akonadi_google_resource ${googleresource_SRCS}) + +if( APPLE ) + set_target_properties(akonadi_google_resource PROPERTIES + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../../Info.plist.template + ) + set_target_properties(akonadi_google_resource PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.google" + ) + set_target_properties(akonadi_google_resource PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Google Resource" + ) +endif() + +target_link_libraries(akonadi_google_resource + KF5::AkonadiCalendar + KF5::AkonadiCore + KF5::AkonadiAgentBase + KF5::CalendarCore + KF5::Contacts + KF5::Wallet + KF5::I18n + KF5::WindowSystem + KF5::Completion + KF5::TextWidgets + KPim::GAPICalendar + KPim::GAPIContacts + KPim::GAPICore + KPim::GAPITasks +) + +install(TARGETS akonadi_google_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +install( + FILES googleresource.desktop + DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" +) diff --git a/resources/google-groupware/calendarhandler.cpp b/resources/google-groupware/calendarhandler.cpp new file mode 100644 index 000000000..bfe6d9ffd --- /dev/null +++ b/resources/google-groupware/calendarhandler.cpp @@ -0,0 +1,402 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "calendarhandler.h" +#include "defaultreminderattribute.h" +#include "googleresource.h" +#include "googlesettings.h" +#include "googlecalendar_debug.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace KGAPI2; +using namespace Akonadi; + +static constexpr uint32_t KGAPIEventVersion = 1; + +QString CalendarHandler::mimeType() +{ + return KCalendarCore::Event::eventMimeType(); +} + +bool CalendarHandler::canPerformTask(const Item &item) +{ + return GenericHandler::canPerformTask(item); +} + +bool CalendarHandler::canPerformTask(const Item::List &items) +{ + return GenericHandler::canPerformTask(items); +} + +void CalendarHandler::setupCollection(Collection &collection, const CalendarPtr &calendar) +{ + collection.setContentMimeTypes({ mimeType() }); + collection.setName(calendar->uid()); + collection.setRemoteId(calendar->uid()); + if (calendar->editable()) { + collection.setRights(Collection::CanChangeCollection + |Collection::CanDeleteCollection + |Collection::CanCreateItem + |Collection::CanChangeItem + |Collection::CanDeleteItem); + } else { + collection.setRights(Collection::ReadOnly); + } + // TODO: for some reason, KOrganizer creates virtual collections + //newCollection.setVirtual(false); + // Setting icon + auto attr = collection.attribute(Collection::AddIfMissing); + attr->setDisplayName(calendar->title()); + attr->setIconName(QStringLiteral("view-calendar")); + // Setting color + if (calendar->backgroundColor().isValid()) { + auto colorAttr = collection.attribute(Collection::AddIfMissing); + colorAttr->setColor(calendar->backgroundColor()); + } + // Setting default remoinders + auto reminderAttr = collection.attribute(Collection::AddIfMissing); + reminderAttr->setReminders(calendar->defaultReminders()); + // Block email reminders, since Google sends them for us + auto blockAlarms = collection.attribute(Collection::AddIfMissing); + blockAlarms->blockAlarmType(KCalendarCore::Alarm::Audio, false); + blockAlarms->blockAlarmType(KCalendarCore::Alarm::Display, false); + blockAlarms->blockAlarmType(KCalendarCore::Alarm::Procedure, false); +} + +void CalendarHandler::retrieveCollections(const Collection &rootCollection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving calendars")); + qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving calendars..."; + auto job = new CalendarFetchJob(m_settings->accountPtr(), this); + connect(job, &CalendarFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + qCDebug(GOOGLE_CALENDAR_LOG) << "Calendars retrieved"; + + const ObjectsList calendars = qobject_cast(job)->items(); + Collection::List collections; + collections.reserve(calendars.count()); + const QStringList activeCalendars = m_settings->calendars(); + for (const auto &object : calendars) { + const CalendarPtr &calendar = object.dynamicCast(); + qCDebug(GOOGLE_CALENDAR_LOG) << " -" << calendar->title() << "(" << calendar->uid() << ")"; + if (!activeCalendars.contains(calendar->uid())) { + qCDebug(GOOGLE_CALENDAR_LOG) << "Skipping, not subscribed"; + continue; + } + Collection collection; + setupCollection(collection, calendar); + collection.setParentCollection(rootCollection); + collections << collection; + } + + m_iface->collectionsRetrievedFromHandler(collections); + }); +} + +void CalendarHandler::retrieveItems(const Collection &collection) +{ + qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieving events for calendar" << collection.remoteId(); + QString syncToken = collection.remoteRevision(); + auto job = new EventFetchJob(collection.remoteId(), m_settings->accountPtr(), this); + if (!syncToken.isEmpty()) { + qCDebug(GOOGLE_CALENDAR_LOG) << "Using sync token" << syncToken; + job->setSyncToken(syncToken); + job->setFetchDeleted(true); + } else { + // No need to fetch deleted items for non-incremental update + job->setFetchDeleted(false); + if (!m_settings->eventsSince().isEmpty()) { + const QDate date = QDate::fromString(m_settings->eventsSince(), Qt::ISODate); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + job->setTimeMin(QDateTime(date).toSecsSinceEpoch()); +#else + job->setTimeMin(QDateTime(date.startOfDay()).toSecsSinceEpoch()); +#endif + } + } + + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &EventFetchJob::finished, this, &CalendarHandler::slotItemsRetrieved); + + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving events for calendar '%1'", collection.displayName())); +} + +void CalendarHandler::slotItemsRetrieved(KGAPI2::Job *job) +{ + if (!m_iface->handleError(job)) { + return; + } + Item::List changedItems, removedItems; + Collection collection = job->property(COLLECTION_PROPERTY).value(); + DefaultReminderAttribute *attr = collection.attribute(); + + auto fetchJob = qobject_cast(job); + const ObjectsList objects = fetchJob->items(); + bool isIncremental = !fetchJob->syncToken().isEmpty(); + qCDebug(GOOGLE_CALENDAR_LOG) << "Retrieved" << objects.count() << "events for calendar" << collection.remoteId(); + for (const ObjectPtr &object : objects) { + const EventPtr event = object.dynamicCast(); + if (event->useDefaultReminders() && attr) { + const KCalendarCore::Alarm::List alarms = attr->alarms(event.data()); + for (const KCalendarCore::Alarm::Ptr &alarm : alarms) { + event->addAlarm(alarm); + } + } + + Item item; + item.setMimeType( mimeType() ); + item.setParentCollection(collection); + item.setRemoteId(event->id()); + item.setRemoteRevision(event->etag()); + item.setPayload(event.dynamicCast()); + + if (event->deleted()) { + qCDebug(GOOGLE_CALENDAR_LOG) << " - removed" << event->uid(); + removedItems << item; + } else { + qCDebug(GOOGLE_CALENDAR_LOG) << " - changed" << event->uid(); + changedItems << item; + } + } + + if (!isIncremental) { + m_iface->itemsRetrieved(changedItems); + } else { + m_iface->itemsRetrievedIncremental(changedItems, removedItems); + } + qCDebug(GOOGLE_CALENDAR_LOG) << "Next sync token:" << fetchJob->syncToken(); + collection.setRemoteRevision(fetchJob->syncToken()); + new CollectionModifyJob(collection, this); + + emitReadyStatus(); +} + +void CalendarHandler::itemAdded(const Item &item, const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding event to calendar '%1'", collection.name())); + qCDebug(GOOGLE_CALENDAR_LOG) << "Event added to calendar" << collection.remoteId(); + KCalendarCore::Event::Ptr event = item.payload(); + EventPtr kevent(new Event(*event)); + auto *job = new EventCreateJob(kevent, collection.remoteId(), m_settings->accountPtr(), this); + job->setSendUpdates(SendUpdatesPolicy::None); + connect(job, &EventCreateJob::finished, this, [this, item](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + Item newItem(item); + const EventPtr event = qobject_cast(job)->items().first().dynamicCast(); + qCDebug(GOOGLE_CALENDAR_LOG) << "Event added"; + newItem.setRemoteId(event->id()); + newItem.setRemoteRevision(event->etag()); + newItem.setGid(event->uid()); + m_iface->itemChangeCommitted(newItem); + newItem.setPayload(event.dynamicCast()); + new ItemModifyJob(newItem, this); + emitReadyStatus(); + }); +} + +void CalendarHandler::itemChanged(const Item &item, const QSet< QByteArray > &/*partIdentifiers*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing event in calendar '%1'", item.parentCollection().displayName())); + qCDebug(GOOGLE_CALENDAR_LOG) << "Changing event" << item.remoteId(); + KCalendarCore::Event::Ptr event = item.payload(); + EventPtr kevent(new Event(*event)); + auto job = new EventModifyJob(kevent, item.parentCollection().remoteId(), m_settings->accountPtr(), this); + job->setSendUpdates(SendUpdatesPolicy::None); + job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); + connect(job, &EventModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished); +} + +void CalendarHandler::itemsRemoved(const Item::List &items) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 events", "Removing %1 event", items.count())); + QStringList eventIds; + eventIds.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), + [](const Item &item){ + return item.remoteId(); + }); + qCDebug(GOOGLE_CALENDAR_LOG) << "Removing events:" << eventIds; + auto job = new EventDeleteJob(eventIds, items.first().parentCollection().remoteId(), m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &EventDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished); +} + +void CalendarHandler::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Moving %1 events from calendar '%2' to calendar '%3'", + "Moving %1 event from calendar '%2' to calendar '%3'", + items.count(), collectionSource.displayName(), collectionDestination.displayName())); + QStringList eventIds; + eventIds.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(eventIds), + [](const Item &item){ + return item.remoteId(); + }); + qCDebug(GOOGLE_CALENDAR_LOG) << "Moving events" << eventIds << "from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); + auto job = new EventMoveJob(eventIds, collectionSource.remoteId(), collectionDestination.remoteId(), m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &EventMoveJob::finished, this, &CalendarHandler::slotGenericJobFinished); +} + +void CalendarHandler::collectionAdded(const Collection &collection, const Collection &/*parent*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating calendar '%1'", collection.displayName())); + qCDebug(GOOGLE_CALENDAR_LOG) << "Adding calendar" << collection.displayName(); + CalendarPtr calendar(new Calendar()); + calendar->setTitle(collection.displayName()); + calendar->setEditable(true); + + auto job = new CalendarCreateJob(calendar, m_settings->accountPtr(), this); + connect(job, &CalendarCreateJob::finished, this, [this, collection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + CalendarPtr calendar = qobject_cast(job)->items().first().dynamicCast(); + qCDebug(GOOGLE_CALENDAR_LOG) << "Created calendar" << calendar->uid(); + // Enable newly added calendar in settings, otherwise user won't see it + m_settings->addCalendar(calendar->uid()); + // TODO: the calendar returned by google is almost empty, i.e. it's not "editable", + // does not contain the color, etc + calendar->setEditable(true); + // Populate remoteId & other stuff + Collection newCollection(collection); + setupCollection(newCollection, calendar); + m_iface->collectionChangeCommitted(newCollection); + emitReadyStatus(); + }); +} + +void CalendarHandler::collectionChanged(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing calendar '%1'", collection.displayName())); + qCDebug(GOOGLE_CALENDAR_LOG) << "Changing calendar" << collection.remoteId(); + CalendarPtr calendar(new Calendar()); + calendar->setUid(collection.remoteId()); + calendar->setTitle(collection.displayName()); + calendar->setEditable(true); + auto job = new CalendarModifyJob(calendar, m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &CalendarModifyJob::finished, this, &CalendarHandler::slotGenericJobFinished); +} + +void CalendarHandler::collectionRemoved(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing calendar '%1'", collection.displayName())); + qCDebug(GOOGLE_CALENDAR_LOG) << "Removing calendar" << collection.remoteId(); + auto job = new CalendarDeleteJob(collection.remoteId(), m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &CalendarDeleteJob::finished, this, &CalendarHandler::slotGenericJobFinished); +} + + +/** + * FreeBusy + */ +FreeBusyHandler::FreeBusyHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings) + : m_iface(iface) + , m_settings(settings) +{ +} + +QDateTime FreeBusyHandler::lastCacheUpdate() const +{ + return QDateTime(); +} + +void FreeBusyHandler::canHandleFreeBusy(const QString &email) +{ + if (m_iface->canPerformTask()) { + m_iface->handlesFreeBusy(email, false); + return; + } + + auto job = new FreeBusyQueryJob(email, + QDateTime::currentDateTimeUtc(), + QDateTime::currentDateTimeUtc().addSecs(3600), + m_settings->accountPtr(), + this); + connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job){ + auto queryJob = qobject_cast(job); + if (!m_iface->handleError(job, false)) { + m_iface->handlesFreeBusy(queryJob->id(), false); + return; + } + m_iface->handlesFreeBusy(queryJob->id(), true); + }); +} + +void FreeBusyHandler::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end) +{ + if (m_iface->canPerformTask()) { + m_iface->freeBusyRetrieved(email, QString(), false, QString()); + return; + } + + auto job = new FreeBusyQueryJob(email, start, end, m_settings->accountPtr(), this); + connect(job, &FreeBusyQueryJob::finished, this, [this](KGAPI2::Job *job) { + auto queryJob = qobject_cast(job); + + if (!m_iface->handleError(job, false)) { + m_iface->freeBusyRetrieved(queryJob->id(), QString(), false, QString()); + return; + } + + KCalendarCore::FreeBusy::Ptr fb(new KCalendarCore::FreeBusy); + fb->setUid(QStringLiteral("%1%2@google.com").arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMddTHHmmssZ")))); + fb->setOrganizer(job->account()->accountName()); + fb->addAttendee(KCalendarCore::Attendee(QString(), queryJob->id())); + // FIXME: is it really sort? + fb->setDateTime(QDateTime::currentDateTimeUtc(), KCalendarCore::IncidenceBase::RoleSort); + const auto ranges = queryJob->busy(); + for (const auto &range : ranges) { + fb->addPeriod(range.busyStart, range.busyEnd); + } + + KCalendarCore::ICalFormat format; + const QString fbStr = format.createScheduleMessage(fb, KCalendarCore::iTIPRequest); + + m_iface->freeBusyRetrieved(queryJob->id(), fbStr, true, QString()); + }); +} diff --git a/resources/google-groupware/calendarhandler.h b/resources/google-groupware/calendarhandler.h new file mode 100644 index 000000000..9426ba0dc --- /dev/null +++ b/resources/google-groupware/calendarhandler.h @@ -0,0 +1,69 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Pobiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef CALENDARHANDLER_H +#define CALENDARHANDLER_H + +#include "generichandler.h" +#include +#include + +class CalendarHandler : public GenericHandler +{ + Q_OBJECT +public: + using GenericHandler::GenericHandler; + + QString mimeType() override; + bool canPerformTask(const Akonadi::Item &item) override; + bool canPerformTask(const Akonadi::Item::List &items) override; + + void retrieveCollections(const Akonadi::Collection &rootCollection) override; + void retrieveItems(const Akonadi::Collection &collection) override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) override; + void itemsRemoved(const Akonadi::Item::List &items) override; + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) override; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + void collectionChanged(const Akonadi::Collection &collection) override; + void collectionRemoved(const Akonadi::Collection &collection) override; +private Q_SLOTS: + void slotItemsRetrieved(KGAPI2::Job *job); +private: + void setupCollection(Akonadi::Collection &collection, const KGAPI2::CalendarPtr &group); +}; + +class FreeBusyHandler : public QObject +{ + Q_OBJECT +public: + typedef std::unique_ptr Ptr; + + FreeBusyHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings); + + QDateTime lastCacheUpdate() const; + void canHandleFreeBusy(const QString &email); + void retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end); +private: + GoogleResourceStateInterface *m_iface = nullptr; + GoogleSettings *m_settings = nullptr; +}; + +#endif // CALENDARHANDLER_H diff --git a/resources/google-groupware/contacthandler.cpp b/resources/google-groupware/contacthandler.cpp new file mode 100644 index 000000000..8e624cb20 --- /dev/null +++ b/resources/google-groupware/contacthandler.cpp @@ -0,0 +1,496 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "contacthandler.h" +#include "googleresource.h" +#include "googlesettings.h" + +#include "googlecontacts_debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define OTHERCONTACTS_REMOTEID QStringLiteral("OtherContacts") + +using namespace KGAPI2; +using namespace Akonadi; + +QString ContactHandler::mimeType() +{ + return KContacts::Addressee::mimeType(); +} + +bool ContactHandler::canPerformTask(const Item &item) +{ + return GenericHandler::canPerformTask(item); +} + +bool ContactHandler::canPerformTask(const Item::List &items) +{ + return GenericHandler::canPerformTask(items); +} + +QString ContactHandler::myContactsRemoteId() const +{ + return QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(m_settings->accountPtr()->accountName()))); +} + +void ContactHandler::setupCollection(Collection &collection, const ContactsGroupPtr &group) +{ + collection.setContentMimeTypes({ mimeType() }); + collection.setName(group->id()); + collection.setRemoteId(group->id()); + + QString realName = group->title(); + if (group->isSystemGroup()) { + if (group->title().contains(QLatin1String("Coworkers"))) { + realName = i18nc("Name of a group of contacts", "Coworkers"); + } else if (group->title().contains(QLatin1String("Friends"))) { + realName = i18nc("Name of a group of contacts", "Friends"); + } else if (group->title().contains(QLatin1String("Family"))) { + realName = i18nc("Name of a group of contacts", "Family"); + } else if (group->title().contains(QLatin1String("My Contacts"))) { + realName = i18nc("Name of a group of contacts", "My Contacts"); + } + } + + // "My Contacts" is the only one not virtual + if (group->id() == myContactsRemoteId()) { + collection.setRights(Collection::CanCreateItem + |Collection::CanChangeItem + |Collection::CanDeleteItem); + } else { + collection.setRights(Collection::CanLinkItem + |Collection::CanUnlinkItem + |Collection::CanChangeItem); + collection.setVirtual(true); + if (!group->isSystemGroup()) { + collection.setRights(collection.rights() + |Collection::CanChangeCollection + |Collection::CanDeleteCollection); + } + } + + auto attr = collection.attribute(Collection::AddIfMissing); + attr->setDisplayName(realName); + attr->setIconName(QStringLiteral("view-pim-contacts")); +} + +void ContactHandler::retrieveCollections(const Collection &rootCollection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving contacts groups")); + qCDebug(GOOGLE_CONTACTS_LOG) << "Retrieving contacts groups..."; + m_collections.clear(); + + Collection otherCollection; + otherCollection.setContentMimeTypes({ mimeType() }); + otherCollection.setName(i18n("Other Contacts")); + otherCollection.setParentCollection(rootCollection); + otherCollection.setRights(Collection::CanCreateItem + |Collection::CanChangeItem + |Collection::CanDeleteItem); + otherCollection.setRemoteId(OTHERCONTACTS_REMOTEID); + + auto attr = otherCollection.attribute(Collection::AddIfMissing); + attr->setDisplayName(i18n("Other Contacts")); + attr->setIconName(QStringLiteral("view-pim-contacts")); + + m_iface->collectionsRetrieved({ otherCollection }); + m_collections[ OTHERCONTACTS_REMOTEID ] = otherCollection; + + auto job = new ContactsGroupFetchJob(m_settings->accountPtr(), this); + connect(job, &ContactFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + qCDebug(GOOGLE_CONTACTS_LOG) << "Contacts groups retrieved"; + + const ObjectsList objects = qobject_cast(job)->items(); + Collection::List collections; + collections.reserve(objects.count()); + std::transform(objects.cbegin(), objects.cend(), std::back_inserter(collections), + [this, &rootCollection](const ObjectPtr &object){ + const ContactsGroupPtr group = object.dynamicCast(); + qCDebug(GOOGLE_CONTACTS_LOG) << " -" << group->title() << "(" << group->id() << ")"; + Collection collection; + setupCollection(collection, group); + collection.setParentCollection(rootCollection); + m_collections[ collection.remoteId() ] = collection; + return collection; + }); + m_iface->collectionsRetrievedFromHandler(collections); + }); +} + +void ContactHandler::retrieveItems(const Collection &collection) +{ + // Contacts are stored inside "My Contacts" and "Other Contacts" only + if ((collection.remoteId() != OTHERCONTACTS_REMOTEID) + && (collection.remoteId() != myContactsRemoteId())) { + m_iface->itemsRetrievalDone(); + return; + } + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving contacts for group '%1'", collection.displayName())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Retreiving contacts for group" << collection.remoteId() << "..."; + + auto job = new ContactFetchJob(m_settings->accountPtr(), this); + if (!collection.remoteRevision().isEmpty()) { + job->setFetchOnlyUpdated(collection.remoteRevision().toLongLong()); + job->setFetchDeleted(true); + } else { + // No need to fetch deleted items for a non-incremental update + job->setFetchDeleted(false); + } + connect(job, &ContactFetchJob::finished, this, &ContactHandler::slotItemsRetrieved); +} + +void ContactHandler::slotItemsRetrieved(KGAPI2::Job *job) +{ + if (!m_iface->handleError(job)) { + return; + } + + Collection collection = m_iface->currentCollection(); + + Item::List changedItems, removedItems; + QHash groupsMap; + QStringList changedPhotos; + + auto fetchJob = qobject_cast(job); + bool isIncremental = (fetchJob->fetchOnlyUpdated() > 0); + const ObjectsList objects = fetchJob->items(); + qCDebug(GOOGLE_CONTACTS_LOG) << "Retrieved" << objects.count() << "contacts"; + for (const ObjectPtr &object : objects) { + const ContactPtr contact = object.dynamicCast(); + + Item item; + item.setMimeType( mimeType() ); + item.setParentCollection(collection); + item.setRemoteId(contact->uid()); + item.setRemoteRevision(contact->etag()); + item.setPayload(*contact.dynamicCast()); + + if (contact->deleted() + || (collection.remoteId() == OTHERCONTACTS_REMOTEID && !contact->groups().isEmpty()) + || (collection.remoteId() == myContactsRemoteId() && contact->groups().isEmpty())) { + qCDebug(GOOGLE_CONTACTS_LOG) << " - removed" << contact->uid(); + removedItems << item; + } else { + qCDebug(GOOGLE_CONTACTS_LOG) << " - changed" << contact->uid(); + changedItems << item; + changedPhotos << contact->uid(); + } + + const QStringList groups = contact->groups(); + for (const QString &group : groups) { + // We don't link contacts to "My Contacts" + if (group != myContactsRemoteId()) { + groupsMap[group] << item; + } + } + } + + if (isIncremental) { + m_iface->itemsRetrievedIncremental(changedItems, removedItems); + } else { + m_iface->itemsRetrieved(changedItems); + } + + for (auto iter = groupsMap.constBegin(), iterEnd = groupsMap.constEnd(); iter != iterEnd; ++iter) { + new LinkJob(m_collections[iter.key()], iter.value(), this); + } + // TODO: unlink if the group was removed! + + if (!changedPhotos.isEmpty()) { + QVariantMap map; + map[QStringLiteral("collection")] = QVariant::fromValue(collection); + map[QStringLiteral("modified")] = QVariant::fromValue(changedPhotos); + m_iface->scheduleCustomTask(this, "retrieveContactsPhotos", map); + } + + const QDateTime local(QDateTime::currentDateTime()); + const QDateTime UTC(local.toUTC()); + + collection.setRemoteRevision(QString::number(UTC.toSecsSinceEpoch())); + new CollectionModifyJob(collection, this); + + emitReadyStatus(); +} + +void ContactHandler::retrieveContactsPhotos(const QVariant &argument) +{ + if (!m_iface->canPerformTask()) { + return; + } + const auto map = argument.value(); + const auto collection = map[QStringLiteral("collection")].value(); + const auto changedPhotos = map[QStringLiteral("modified")].toStringList(); + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Retrieving %1 contacts photos for group '%2'", + "Retrieving %1 contact photo for group '%2'", + changedPhotos.count(), collection.displayName())); + + Item::List items; + items.reserve(changedPhotos.size()); + std::transform(changedPhotos.cbegin(), changedPhotos.cend(), std::back_inserter(items), + [](const QString &contact){ + Item item; + item.setRemoteId(contact); + return item; + }); + auto job = new ItemFetchJob(items, this); + job->setCollection(collection); + job->fetchScope().fetchFullPayload(true); + connect(job, &ItemFetchJob::finished, this, &ContactHandler::slotUpdatePhotosItemsRetrieved); +} + +void ContactHandler::slotUpdatePhotosItemsRetrieved(KJob *job) +{ + // Make sure account is still valid + if (!m_iface->canPerformTask()) { + return; + } + const Item::List items = qobject_cast(job)->items(); + ContactsList contacts; + qCDebug(GOOGLE_CONTACTS_LOG) << "Fetched" << items.count() << "contacts for photo update"; + contacts.reserve(items.size()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), + [](const Item &item){ + const KContacts::Addressee addressee = item.payload(); + const ContactPtr contact(new Contact(addressee)); + return contact; + }); + + qCDebug(GOOGLE_CONTACTS_LOG) << "Starting fetching photos..."; + auto photoJob = new ContactFetchPhotoJob(contacts, m_settings->accountPtr(), this); + photoJob->setProperty("processedItems", 0); + connect(photoJob, &ContactFetchPhotoJob::photoFetched, this, [this, items](KGAPI2::Job *job, const ContactPtr &contact){ + qCDebug(GOOGLE_CONTACTS_LOG) << " - fetched photo for contact" << contact->uid(); + int processedItems = job->property("processedItems").toInt(); + processedItems++; + job->setProperty("processedItems", processedItems); + m_iface->emitPercent(100.0f*processedItems / items.count()); + + auto it = std::find_if(items.cbegin(), items.cend(), [&contact](const Item &item){ + return item.remoteId() == contact->uid(); + }); + if (it != items.cend()) { + Item newItem(*it); + newItem.setPayload(*contact.dynamicCast()); + new ItemModifyJob(newItem, this); + } + }); + + connect(photoJob, &ContactFetchPhotoJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::itemAdded(const Item &item, const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding contact to group '%1'", collection.displayName())); + auto addressee = item.payload< KContacts::Addressee >(); + ContactPtr contact(new Contact(addressee)); + qCDebug(GOOGLE_CONTACTS_LOG) << "Creating contact"; + + if (collection.remoteId() == myContactsRemoteId()) { + contact->addGroup(myContactsRemoteId()); + } + + auto job = new ContactCreateJob(contact, m_settings->accountPtr(), this); + connect(job, &ContactCreateJob::finished, this, [this, item](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + ContactPtr contact = qobject_cast(job)->items().first().dynamicCast(); + Item newItem = item; + qCDebug(GOOGLE_CONTACTS_LOG) << "Contact" << contact->uid() << "created"; + newItem.setRemoteId(contact->uid()); + newItem.setRemoteRevision(contact->etag()); + m_iface->itemChangeCommitted(newItem); + newItem.setPayload(*contact.dynamicCast()); + new ItemModifyJob(newItem, this); + emitReadyStatus(); + }); +} + +void ContactHandler::itemChanged(const Item &item, const QSet< QByteArray > &/*partIdentifiers*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing contact")); + qCDebug(GOOGLE_CONTACTS_LOG) << "Changing contact" << item.remoteId(); + KContacts::Addressee addressee = item.payload< KContacts::Addressee >(); + ContactPtr contact(new Contact(addressee)); + auto job = new ContactModifyJob(contact, m_settings->accountPtr(), this); + job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); + connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::itemsRemoved(const Item::List &items) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 contacts", "Removing %1 contact", items.count())); + QStringList contactIds; + contactIds.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(contactIds), + [](const Item &item){ + return item.remoteId(); + }); + qCDebug(GOOGLE_CONTACTS_LOG) << "Removing contacts" << contactIds; + auto job = new ContactDeleteJob(contactIds, m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &ContactDeleteJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination) +{ + qCDebug(GOOGLE_CONTACTS_LOG) << "Moving contacts from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); + if (!(((collectionSource.remoteId() == myContactsRemoteId()) && (collectionDestination.remoteId() == OTHERCONTACTS_REMOTEID)) || + ((collectionSource.remoteId() == OTHERCONTACTS_REMOTEID) && (collectionDestination.remoteId() == myContactsRemoteId())))) { + m_iface->cancelTask(i18n("Invalid source or destination collection")); + } + + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Moving %1 contacts from group '%2' to '%3'", + "Moving %1 contact from group '%2' to '%3'", + items.count(), collectionSource.remoteId(), collectionDestination.remoteId())); + ContactsList contacts; + contacts.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), + [this, &collectionSource, &collectionDestination](const Item &item){ + KContacts::Addressee addressee = item.payload(); + ContactPtr contact(new Contact(addressee)); + // MyContacts -> OtherContacts + if (collectionSource.remoteId() == myContactsRemoteId() + && collectionDestination.remoteId() == OTHERCONTACTS_REMOTEID) { + contact->clearGroups(); + // OtherContacts -> MyContacts + } else if (collectionSource.remoteId() == OTHERCONTACTS_REMOTEID + && collectionDestination.remoteId() == myContactsRemoteId()) { + contact->addGroup(myContactsRemoteId()); + } + + return contact; + }); + qCDebug(GOOGLE_CONTACTS_LOG) << "Moving contacts from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); + auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::itemsLinked(const Item::List &items, const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Linking %1 contact", "Linking %1 contacts", items.count())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Linking" << items.count() << "contacts to group" << collection.remoteId(); + + ContactsList contacts; + contacts.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), + [this, &collection](const Item &item){ + KContacts::Addressee addressee = item.payload(); + ContactPtr contact(new Contact(addressee)); + contact->addGroup(collection.remoteId()); + return contact; + }); + auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::itemsUnlinked(const Item::List &items, const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Unlinking %1 contact", "Unlinking %1 contacts", items.count())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Unlinking" << items.count() << "contacts from group" << collection.remoteId(); + + ContactsList contacts; + contacts.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), + [this, &collection](const Item &item){ + KContacts::Addressee addressee = item.payload(); + ContactPtr contact(new Contact(addressee)); + contact->removeGroup(collection.remoteId()); + return contact; + }); + auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + + +void ContactHandler::collectionAdded(const Collection &collection, const Collection & /*parent*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating new contact group '%1'", collection.displayName())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Adding contact group" << collection.displayName(); + ContactsGroupPtr group(new ContactsGroup); + group->setTitle(collection.name()); + group->setIsSystemGroup(false); + + auto job = new ContactsGroupCreateJob(group, m_settings->accountPtr(), this); + connect(job, &ContactsGroupCreateJob::finished, this, [this, collection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + + ContactsGroupPtr group = qobject_cast(job)->items().first().dynamicCast(); + qCDebug(GOOGLE_CONTACTS_LOG) << "Contact group created:" << group->id(); + Collection newCollection(collection); + setupCollection(newCollection, group); + m_collections[ newCollection.remoteId() ] = newCollection; + m_iface->collectionChangeCommitted(newCollection); + emitReadyStatus(); + }); +} + +void ContactHandler::collectionChanged(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing contact group '%1'", collection.displayName())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Changing contact group" << collection.remoteId(); + + ContactsGroupPtr group(new ContactsGroup()); + group->setId(collection.remoteId()); + group->setTitle(collection.displayName()); + + auto job = new ContactsGroupModifyJob(group, m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &ContactsGroupModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); +} + +void ContactHandler::collectionRemoved(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing contact group '%1'", collection.displayName())); + qCDebug(GOOGLE_CONTACTS_LOG) << "Removing contact group" << collection.remoteId(); + auto job = new ContactsGroupDeleteJob(collection.remoteId(), m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &ContactsGroupDeleteJob::finished, this, &ContactHandler::slotGenericJobFinished); +} diff --git a/resources/google-groupware/contacthandler.h b/resources/google-groupware/contacthandler.h new file mode 100644 index 000000000..86c7ac726 --- /dev/null +++ b/resources/google-groupware/contacthandler.h @@ -0,0 +1,61 @@ +/* + Copyright (C) 2011, 2012 Dan Vratil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef CONTACTHANDLER_H +#define CONTACTHANDLER_H + +#include "generichandler.h" +#include +#include + + +class ContactHandler : public GenericHandler +{ + Q_OBJECT +public: + using GenericHandler::GenericHandler; + + QString mimeType() override; + bool canPerformTask(const Akonadi::Item &item) override; + bool canPerformTask(const Akonadi::Item::List &items) override; + + void retrieveCollections(const Akonadi::Collection &rootCollection) override; + void retrieveItems(const Akonadi::Collection &collection) override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) override; + void itemsRemoved(const Akonadi::Item::List &items) override; + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) override; + void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) override; + void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) override; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + void collectionChanged(const Akonadi::Collection &collection) override; + void collectionRemoved(const Akonadi::Collection &collection) override; +private Q_SLOTS: + void slotItemsRetrieved(KGAPI2::Job *job); + void slotUpdatePhotosItemsRetrieved(KJob *job); + void retrieveContactsPhotos(const QVariant &arguments); +private: + QString myContactsRemoteId() const; + void setupCollection(Akonadi::Collection &collection, const KGAPI2::ContactsGroupPtr &group); + QMap m_collections; +}; + +#endif // CONTACTHANDLER_H diff --git a/resources/google-groupware/defaultreminderattribute.cpp b/resources/google-groupware/defaultreminderattribute.cpp new file mode 100644 index 000000000..04e1e6617 --- /dev/null +++ b/resources/google-groupware/defaultreminderattribute.cpp @@ -0,0 +1,115 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "defaultreminderattribute.h" + +#include +#include + +#include + +using namespace KGAPI2; + +DefaultReminderAttribute::DefaultReminderAttribute() +{ +} + +Akonadi::Attribute *DefaultReminderAttribute::clone() const +{ + DefaultReminderAttribute *attr = new DefaultReminderAttribute(); + attr->setReminders(m_reminders); + + return attr; +} + +void DefaultReminderAttribute::setReminders(const RemindersList &reminders) +{ + m_reminders = reminders; +} + +void DefaultReminderAttribute::deserialize(const QByteArray &data) +{ + QJsonDocument json = QJsonDocument::fromJson(data); + if (json.isNull()) { + return; + } + + const QVariant var = json.toVariant(); + const QVariantList list = var.toList(); + for (const QVariant &l : list) { + QVariantMap reminder = l.toMap(); + + KGAPI2::ReminderPtr rem(new KGAPI2::Reminder); + + if (reminder[QStringLiteral("type")].toString() == QLatin1String("display")) { + rem->setType(KCalendarCore::Alarm::Display); + } else if (reminder[QStringLiteral("type")].toString() == QLatin1String("email")) { + rem->setType(KCalendarCore::Alarm::Email); + } + + KCalendarCore::Duration offset(reminder[QStringLiteral("time")].toInt(), KCalendarCore::Duration::Seconds); + rem->setStartOffset(offset); + + m_reminders << rem; + } +} + +QByteArray DefaultReminderAttribute::serialized() const +{ + QVariantList list; + list.reserve(m_reminders.count()); + + for (const ReminderPtr &rem : qAsConst(m_reminders)) { + QVariantMap reminder; + + if (rem->type() == KCalendarCore::Alarm::Display) { + reminder[QStringLiteral("type")] = QLatin1String("display"); + } else if (rem->type() == KCalendarCore::Alarm::Email) { + reminder[QStringLiteral("type")] = QLatin1String("email"); + } + + reminder[QStringLiteral("time")] = rem->startOffset().asSeconds(); + + list << reminder; + } + QJsonDocument serialized = QJsonDocument::fromVariant(list); + return serialized.toJson(); +} + +KCalendarCore::Alarm::List DefaultReminderAttribute::alarms(KCalendarCore::Incidence *incidence) const +{ + KCalendarCore::Alarm::List alarms; + alarms.reserve(m_reminders.count()); + for (const ReminderPtr &reminder : qAsConst(m_reminders)) { + KCalendarCore::Alarm::Ptr alarm(new KCalendarCore::Alarm(incidence)); + + alarm->setType(reminder->type()); + alarm->setTime(incidence->dtStart()); + alarm->setStartOffset(reminder->startOffset()); + alarm->setEnabled(true); + + alarms << alarm; + } + + return alarms; +} + +QByteArray DefaultReminderAttribute::type() const +{ + static const QByteArray sType("defaultReminders"); + return sType; +} diff --git a/resources/google-groupware/defaultreminderattribute.h b/resources/google-groupware/defaultreminderattribute.h new file mode 100644 index 000000000..625498669 --- /dev/null +++ b/resources/google-groupware/defaultreminderattribute.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GOOGLE_CALENDAR_DEFAULTREMINDERATTRIBUTE_H +#define GOOGLE_CALENDAR_DEFAULTREMINDERATTRIBUTE_H + +#include + +#include + +#include +#include + +class DefaultReminderAttribute : public Akonadi::Attribute +{ +public: + explicit DefaultReminderAttribute(); + + Attribute *clone() const override; + void deserialize(const QByteArray &data) override; + QByteArray serialized() const override; + QByteArray type() const override; + + void setReminders(const KGAPI2::RemindersList &reminders); + KCalendarCore::Alarm::List alarms(KCalendarCore::Incidence *incidence) const; + +private: + KGAPI2::RemindersList m_reminders; +}; + +#endif // DEFAULTREMINDERATTRIBUTE_H diff --git a/resources/google-groupware/generichandler.cpp b/resources/google-groupware/generichandler.cpp new file mode 100644 index 000000000..3531338ea --- /dev/null +++ b/resources/google-groupware/generichandler.cpp @@ -0,0 +1,71 @@ +/* + Copyright (C) 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "generichandler.h" +#include "googleresourcestateinterface.h" +#include "googleresource_debug.h" + +#include + +#include + +GenericHandler::GenericHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings) + : m_iface(iface) + , m_settings(settings) +{ +} + +GenericHandler::~GenericHandler() = default; + +void GenericHandler::itemsLinked(const Akonadi::Item::List &/*items*/, const Akonadi::Collection &/*collection*/) +{ + m_iface->cancelTask(i18n("Cannot handle item linking")); +} + +void GenericHandler::itemsUnlinked(const Akonadi::Item::List &/*items*/, const Akonadi::Collection &/*collection*/) +{ + m_iface->cancelTask(i18n("Cannot handle item unlinking")); +} + +void GenericHandler::slotGenericJobFinished(KGAPI2::Job *job) +{ + if (!m_iface->handleError(job)) { + return; + } + if (job->property(ITEM_PROPERTY).isValid()) { + qCDebug(GOOGLE_LOG) << "Item change committed"; + m_iface->itemChangeCommitted(job->property(ITEM_PROPERTY).value()); + } else if (job->property(ITEMS_PROPERTY).isValid()) { + qCDebug(GOOGLE_LOG) << "Items changes committed"; + m_iface->itemsChangesCommitted(job->property(ITEMS_PROPERTY).value()); + } else if (job->property(COLLECTION_PROPERTY).isValid()) { + qCDebug(GOOGLE_LOG) << "Collection change committed"; + m_iface->collectionChangeCommitted(job->property(COLLECTION_PROPERTY).value()); + } else { + qCDebug(GOOGLE_LOG) << "Task done"; + m_iface->taskDone(); + } + + emitReadyStatus(); +} + +void GenericHandler::emitReadyStatus() +{ + m_iface->emitStatus(Akonadi::AgentBase::Idle, i18nc("@status", "Ready")); +} + +#include "moc_generichandler.cpp" diff --git a/resources/google-groupware/generichandler.h b/resources/google-groupware/generichandler.h new file mode 100644 index 000000000..f05f98f89 --- /dev/null +++ b/resources/google-groupware/generichandler.h @@ -0,0 +1,104 @@ +/* + Copyright (C) 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GENERICHANDLER_H +#define GENERICHANDLER_H + +#include + +#include +#include + +#include +#include + +#include + +#include "googleresourcestateinterface.h" + +#define ITEM_PROPERTY "_AkonadiItem" +#define ITEMS_PROPERTY "_AkonadiItems" +#define COLLECTION_PROPERTY "_AkonadiCollection" + +namespace KGAPI2 { + class Job; +} + +class GoogleSettings; + +class GenericHandler : public QObject +{ + Q_OBJECT +public: + typedef std::unique_ptr Ptr; + + GenericHandler(GoogleResourceStateInterface *iface, GoogleSettings *settings); + virtual ~GenericHandler(); + + virtual QString mimeType() = 0; + + virtual void retrieveCollections(const Akonadi::Collection &rootCollection) = 0; + virtual void retrieveItems(const Akonadi::Collection &collection) = 0; + + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) = 0; + virtual void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) = 0; + virtual void itemsRemoved(const Akonadi::Item::List &items) = 0; + virtual void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) = 0; + virtual void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + virtual void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) = 0; + virtual void collectionChanged(const Akonadi::Collection &collection) = 0; + virtual void collectionRemoved(const Akonadi::Collection &collection) = 0; + + /* + * Helper function for various handlers + */ + template + bool canPerformTask(const Akonadi::Item &item) + { + if (item.isValid() && (!item.hasPayload() || item.mimeType() != mimeType())) { + m_iface->cancelTask(i18n("Invalid item.")); + return false; + } + return m_iface->canPerformTask(); + } + + template + bool canPerformTask(const Akonadi::Item::List &items) + { + if (std::any_of(items.cbegin(), items.cend(), [this](const Akonadi::Item &item){ + return item.isValid() && (!item.hasPayload() || item.mimeType() != mimeType()); + })) { + m_iface->cancelTask(i18n("Invalid item.")); + return false; + } + return m_iface->canPerformTask(); + } + + virtual bool canPerformTask(const Akonadi::Item &item) = 0; + virtual bool canPerformTask(const Akonadi::Item::List &items) = 0; +protected Q_SLOTS: + void slotGenericJobFinished(KGAPI2::Job *job); +protected: + void emitReadyStatus(); + + GoogleResourceStateInterface *m_iface = nullptr; + GoogleSettings *m_settings = nullptr; +}; + +#endif // GENERICHANDLER_H diff --git a/resources/google-groupware/googleresource.cpp b/resources/google-groupware/googleresource.cpp new file mode 100644 index 000000000..b1b0bc813 --- /dev/null +++ b/resources/google-groupware/googleresource.cpp @@ -0,0 +1,495 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "googleresource.h" +#include "googleresourcestate.h" +#include "googlesettings.h" +#include "googlesettingsdialog.h" +#include "googleresource_debug.h" +#include "settingsadaptor.h" + +#include "calendarhandler.h" +#include "contacthandler.h" +#include "taskhandler.h" + +#include "defaultreminderattribute.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + + +#define CALENDARS_PROPERTY "_KGAPI2CalendarPtr" +#define ROOT_COLLECTION_REMOTEID QStringLiteral("RootCollection") + + +Q_DECLARE_METATYPE(KGAPI2::Job *) + +using namespace KGAPI2; +using namespace Akonadi; + +GoogleResource::GoogleResource(const QString &id) + : ResourceBase(id) + , AgentBase::ObserverV3() + , m_iface(new GoogleResourceState(this)) +{ + AttributeFactory::registerAttribute< DefaultReminderAttribute >(); + + connect(this, &GoogleResource::reloadConfiguration, this, &GoogleResource::reloadConfig); + + setNeedsNetwork(true); + + changeRecorder()->itemFetchScope().fetchFullPayload(true); + changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All); + changeRecorder()->fetchCollection(true); + changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All); + + m_settings = new GoogleSettings(); + m_settings->setWindowId(winIdForDialogs()); + connect(m_settings, &GoogleSettings::accountReady, this, [this](bool ready){ + if (accountId() > 0) { + return; + } + if (!ready) { + Q_EMIT status(Broken, i18n("Can't access KWallet")); + return; + } + if (m_settings->accountPtr().isNull()) { + Q_EMIT status(NotConfigured); + return; + } + emitReadyStatus(); + synchronize(); + }); + + Q_EMIT status(NotConfigured, i18n("Waiting for KWallet...")); + updateResourceName(); + + m_freeBusyHandler.reset(new FreeBusyHandler(m_iface, m_settings)); + m_handlers.clear(); + m_handlers.push_back(GenericHandler::Ptr(new CalendarHandler(m_iface, m_settings))); + m_handlers.push_back(GenericHandler::Ptr(new ContactHandler(m_iface, m_settings))); + m_handlers.push_back(GenericHandler::Ptr(new TaskHandler(m_iface, m_settings))); + + new SettingsAdaptor(m_settings); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), + m_settings, QDBusConnection::ExportAdaptors); +} + +GoogleResource::~GoogleResource() +{ + delete m_iface; +} + +void GoogleResource::cleanup() +{ + m_settings->cleanup(); + ResourceBase::cleanup(); +} + +void GoogleResource::emitReadyStatus() +{ + Q_EMIT status(Idle, i18nc("@info:status", "Ready")); +} + +void GoogleResource::configure(WId windowId) +{ + if (!m_settings->isReady() || m_isConfiguring) { + Q_EMIT configurationDialogAccepted(); + return; + } + + m_isConfiguring = true; + + QScopedPointer settingsDialog(new GoogleSettingsDialog(this, m_settings, windowId)); + settingsDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("im-google"))); + if (settingsDialog->exec() == QDialog::Accepted) { + updateResourceName(); + + Q_EMIT configurationDialogAccepted(); + + if (m_settings->accountPtr().isNull()) { + Q_EMIT status(NotConfigured, i18n("Configured account does not exist")); + m_isConfiguring = false; + return; + } + + emitReadyStatus(); + synchronize(); + } else { + updateResourceName(); + + Q_EMIT configurationDialogRejected(); + } + + m_isConfiguring = false; +} + +QList GoogleResource::scopes() const +{ + // TODO: determine it based on what user wants? + const QList< QUrl > scopes = {Account::accountInfoScopeUrl(), Account::calendarScopeUrl(), + Account::contactsScopeUrl(), Account::tasksScopeUrl()}; + return scopes; +} + +void GoogleResource::updateResourceName() +{ + const QString accountName = m_settings->account(); + setName(i18nc("%1 is account name (user@gmail.com)", "Google Groupware (%1)", accountName.isEmpty() ? i18n("not configured") : accountName)); +} + +void GoogleResource::reloadConfig() +{ + const AccountPtr account = m_settings->accountPtr(); + if (account.isNull() || account->accountName().isEmpty()) { + Q_EMIT status(NotConfigured, i18n("Configured account does not exist")); + } else { + emitReadyStatus(); + } +} + +bool GoogleResource::handleError(KGAPI2::Job *job, bool _cancelTask) +{ + if ((job->error() == KGAPI2::NoError) || (job->error() == KGAPI2::OK)) { + return true; + } + qCWarning(GOOGLE_LOG) << "Got error:" << job << job->errorString(); + AccountPtr account = job->account(); + if (job->error() == KGAPI2::Unauthorized) { + const QList resourceScopes = scopes(); + for (const QUrl &scope : resourceScopes) { + if (!account->scopes().contains(scope)) { + account->addScope(scope); + } + } + + AuthJob *authJob = new AuthJob(account, m_settings->clientId(), m_settings->clientSecret(), this); + authJob->setProperty(JOB_PROPERTY, QVariant::fromValue(job)); + connect(authJob, &AuthJob::finished, this, &GoogleResource::slotAuthJobFinished); + return false; + } + + if (_cancelTask) { + cancelTask(job->errorString()); + } + return false; +} + +bool GoogleResource::canPerformTask() +{ + if (!m_settings->accountPtr() && accountId() == 0) { + cancelTask(i18nc("@info:status", "Resource is not configured")); + Q_EMIT status(NotConfigured, i18nc("@info:status", "Resource is not configured")); + return false; + } + + return true; +} + +void GoogleResource::slotAuthJobFinished(KGAPI2::Job *job) +{ + if (job->error() != KGAPI2::NoError) { + cancelTask(i18n("Failed to refresh tokens")); + return; + } + + AuthJob *authJob = qobject_cast(job); + AccountPtr account = authJob->account(); + if (!m_settings->storeAccount(account)) { + qCWarning(GOOGLE_LOG) << "Failed to store account in KWallet"; + } + + KGAPI2::Job *otherJob = job->property(JOB_PROPERTY).value(); + if (otherJob) { + otherJob->setAccount(account); + otherJob->restart(); + } +} + +int GoogleResource::accountId() const +{ + return 0; +} + +GenericHandler *GoogleResource::fetchHandlerByMimetype(const QString &mimeType) +{ + auto it = std::find_if(m_handlers.cbegin(), m_handlers.cend(), + [&mimeType](const GenericHandler::Ptr &handler){ + return handler->mimeType() == mimeType; + }); + + if (it != m_handlers.cend()) { + return it->get(); + } else { + return nullptr; + } +} + +GenericHandler *GoogleResource::fetchHandlerForCollection(const Akonadi::Collection &collection) +{ + auto it = std::find_if(m_handlers.cbegin(), m_handlers.cend(), + [&collection](const GenericHandler::Ptr &handler){ + return collection.contentMimeTypes().contains(handler->mimeType()); + }); + if (it != m_handlers.cend()) { + return it->get(); + } else { + return nullptr; + } +} + +/* + * FreeBusy handling + */ + +QDateTime GoogleResource::lastCacheUpdate() const +{ + if (m_freeBusyHandler) { + return m_freeBusyHandler->lastCacheUpdate(); + } + return QDateTime(); +} + +void GoogleResource::canHandleFreeBusy(const QString &email) const +{ + if (m_freeBusyHandler) { + m_freeBusyHandler->canHandleFreeBusy(email); + } else { + handlesFreeBusy(email, false); + } +} + +void GoogleResource::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end) +{ + if (m_freeBusyHandler) { + m_freeBusyHandler->retrieveFreeBusy(email, start, end); + } else { + freeBusyRetrieved(email, QString(), false, QString()); + } +} + +/* + * Collection handling + */ +void GoogleResource::retrieveCollections() +{ + if (!canPerformTask()) { + return; + } + qCDebug(GOOGLE_LOG) << "Retrieve Collections"; + + setCollectionStreamingEnabled(true); + CachePolicy cachePolicy; + if (m_settings->enableIntervalCheck()) { + cachePolicy.setInheritFromParent(false); + cachePolicy.setIntervalCheckTime(m_settings->intervalCheckTime()); + } + + // Setting up root collection + m_rootCollection = Collection(); + m_rootCollection.setContentMimeTypes({ Collection::mimeType(), Collection::virtualMimeType() }); + m_rootCollection.setRemoteId(ROOT_COLLECTION_REMOTEID); + m_rootCollection.setName(m_settings->accountPtr()->accountName()); + m_rootCollection.setParentCollection(Collection::root()); + m_rootCollection.setRights(Collection::CanCreateCollection); + m_rootCollection.setCachePolicy(cachePolicy); + + EntityDisplayAttribute *attr = m_rootCollection.attribute(Collection::AddIfMissing); + attr->setDisplayName(m_settings->accountPtr()->accountName()); + attr->setIconName(QStringLiteral("im-google")); + + collectionsRetrieved({ m_rootCollection }); + + m_jobs = m_handlers.size(); + for (const auto &handler : m_handlers) { + handler->retrieveCollections(m_rootCollection); + } +} + +void GoogleResource::collectionsRetrievedFromHandler(const Collection::List &collections) +{ + collectionsRetrieved(collections); + m_jobs--; + if (m_jobs == 0) { + qCDebug(GOOGLE_LOG) << "All collections retrieved!"; + collectionsRetrievalDone(); + //taskDone(); // ??? + emitReadyStatus(); + } +} + +void GoogleResource::retrieveItems(const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + + auto handler = fetchHandlerForCollection(collection); + if (handler) { + handler->retrieveItems(collection); + } else { + qCWarning(GOOGLE_LOG) << "Unknown collection" << collection.name(); + itemsRetrieved({}); + } +} + +void GoogleResource::itemAdded(const Item &item, const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + + auto handler = fetchHandlerByMimetype(item.mimeType()); + if (handler && handler->canPerformTask(item)) { + handler->itemAdded(item, collection); + } else { + qCWarning(GOOGLE_LOG) << "Could not add item" << item.mimeType(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::itemChanged(const Item &item, const QSet< QByteArray > &partIdentifiers) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerByMimetype(item.mimeType()); + if (handler && handler->canPerformTask(item)) { + handler->itemChanged(item, partIdentifiers); + } else { + qCWarning(GOOGLE_LOG) << "Could not change item" << item.mimeType(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::itemsRemoved(const Item::List &items) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerByMimetype(items.first().mimeType()); + if (handler && handler->canPerformTask(items)) { + handler->itemsRemoved(items); + } else { + qCWarning(GOOGLE_LOG) << "Could not remove item" << items.first().mimeType(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerByMimetype(items.first().mimeType()); + if (handler && handler->canPerformTask(items)) { + handler->itemsMoved(items, collectionSource, collectionDestination); + } else if (!handler) { + qCWarning(GOOGLE_LOG) << "Could not move item" << items.first().mimeType() << "from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::itemsLinked(const Item::List &items, const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerByMimetype(items.first().mimeType()); + if (handler && handler->canPerformTask(items)) { + handler->itemsLinked(items, collection); + } else if (!handler) { + qCWarning(GOOGLE_LOG) << "Could not link item" << items.first().mimeType() << "to" << collection.remoteId(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::itemsUnlinked(const Item::List &items, const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerByMimetype(items.first().mimeType()); + if (handler && handler->canPerformTask(items)) { + handler->itemsUnlinked(items, collection); + } else if (!handler) { + qCWarning(GOOGLE_LOG) << "Could not unlink item mimetype" << items.first().mimeType() << "from" << collection.remoteId(); + cancelTask(i18n("Invalid payload type")); + } +} + +void GoogleResource::collectionAdded(const Collection &collection, const Collection &parent) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerForCollection(collection); + if (handler) { + handler->collectionAdded(collection, parent); + } else { + qCWarning(GOOGLE_LOG) << "Could not add collection" << collection.displayName() << "mimetypes:" << collection.contentMimeTypes(); + cancelTask(i18n("Unknown collection mimetype")); + } +} + +void GoogleResource::collectionChanged(const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerForCollection(collection); + if (handler) { + handler->collectionChanged(collection); + } else { + qCWarning(GOOGLE_LOG) << "Could not change collection" << collection.displayName() << "mimetypes:" << collection.contentMimeTypes(); + cancelTask(i18n("Unknown collection mimetype")); + } +} + +void GoogleResource::collectionRemoved(const Collection &collection) +{ + if (!canPerformTask()) { + return; + } + auto handler = fetchHandlerForCollection(collection); + if (handler) { + handler->collectionRemoved(collection); + } else { + qCWarning(GOOGLE_LOG) << "Could not remove collection" << collection.displayName() << "mimetypes:" << collection.contentMimeTypes(); + cancelTask(i18n("Unknown collection mimetype")); + } +} + +AKONADI_RESOURCE_MAIN(GoogleResource) diff --git a/resources/google-groupware/googleresource.desktop b/resources/google-groupware/googleresource.desktop new file mode 100644 index 000000000..37d486f65 --- /dev/null +++ b/resources/google-groupware/googleresource.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Google Groupware +Comment=Access your Google Calendars, Contacts and Tasks from KDE +Type=AkonadiResource +Exec=akonadi_google_resource +X-Akonadi-MimeTypes=text/calendar,text/directory,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.freebusy +X-Akonadi-Capabilities=Resource,FreeBusyProvider +X-Akonadi-Identifier=akonadi_google_resource +X-Akonadi-Custom-KAccounts=google-contacts,google-calendar +Icon=im-google diff --git a/resources/google-groupware/googleresource.h b/resources/google-groupware/googleresource.h new file mode 100644 index 000000000..022098196 --- /dev/null +++ b/resources/google-groupware/googleresource.h @@ -0,0 +1,109 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GOOGLERESOURCE_H +#define GOOGLERESOURCE_H + +#include +#include +#include + +#include + +#include "calendarhandler.h" +#include "generichandler.h" + +#include + +#define JOB_PROPERTY "_KGAPI2Job" + +namespace KGAPI2 { +class Job; +} + +class GoogleSettings; +class GoogleResourceState; + +class GoogleResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV3, public Akonadi::FreeBusyProviderBase +{ + Q_OBJECT + +public: + explicit GoogleResource(const QString &id); + ~GoogleResource() override; + + QList scopes() const; + + void cleanup() override; +public Q_SLOTS: + void configure(WId windowId) override; + void reloadConfig(); +protected: + int runConfigurationDialog(WId windowId); + void updateResourceName(); + // Freebusy + QDateTime lastCacheUpdate() const override; + void canHandleFreeBusy(const QString &email) const override; + void retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end) override; + + bool canPerformTask(); + /** + * KAccounts support abstraction. + * + * Returns 0 when compiled without KAccounts or not configured for KAccounts + */ + int accountId() const; + + void emitReadyStatus(); + void collectionsRetrievedFromHandler(const Akonadi::Collection::List &collections); +protected Q_SLOTS: + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &collection) override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) override; + void itemsRemoved(const Akonadi::Item::List &items) override; + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) override; + void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) override; + void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) override; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + void collectionChanged(const Akonadi::Collection &collection) override; + void collectionRemoved(const Akonadi::Collection &collection) override; + + bool handleError(KGAPI2::Job *job, bool cancelTask = true); + + void slotAuthJobFinished(KGAPI2::Job *job); +private: + bool m_isConfiguring = false; + GoogleSettings *m_settings = nullptr; + Akonadi::Collection m_rootCollection; + + GoogleResourceState *m_iface; + + std::vector m_handlers; + FreeBusyHandler::Ptr m_freeBusyHandler; + int m_jobs; + + friend class GoogleSettingsDialog; + friend class GoogleResourceState; + + GenericHandler *fetchHandlerByMimetype(const QString &mimeType); + GenericHandler *fetchHandlerForCollection(const Akonadi::Collection &collection); +}; + +#endif // GOOGLERESOURCE_H diff --git a/resources/google-groupware/googleresourcestate.cpp b/resources/google-groupware/googleresourcestate.cpp new file mode 100644 index 000000000..a67124ec1 --- /dev/null +++ b/resources/google-groupware/googleresourcestate.cpp @@ -0,0 +1,182 @@ +/* + Copyright (c) 2020 Igor Poboiko + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "googleresourcestate.h" +#include "googleresource.h" + +using namespace Akonadi; + +GoogleResourceState::GoogleResourceState(GoogleResource *resource) + : m_resource(resource) +{ +} + +// Items handling +void GoogleResourceState::itemRetrieved(const Item &item) +{ + m_resource->itemRetrieved(item); +} + +void GoogleResourceState::itemsRetrieved(const Item::List &items) +{ + m_resource->itemsRetrieved(items); +} + +void GoogleResourceState::itemsRetrievedIncremental(const Item::List &changed, const Item::List &removed) +{ + m_resource->itemsRetrievedIncremental(changed, removed); +} + +void GoogleResourceState::itemsRetrievalDone() +{ + m_resource->itemsRetrievalDone(); +} + +void GoogleResourceState::setTotalItems(int amount) +{ + m_resource->setTotalItems(amount); + +} +void GoogleResourceState::itemChangeCommitted(const Item &item) +{ + m_resource->changeCommitted(item); +} + +void GoogleResourceState::itemsChangesCommitted(const Item::List &items) +{ + m_resource->changesCommitted(items); +} + +Item::List GoogleResourceState::currentItems() +{ + return m_resource->currentItems(); +} + +// Collections handling +void GoogleResourceState::collectionsRetrieved(const Collection::List &collections) +{ + m_resource->collectionsRetrieved(collections); +} + +void GoogleResourceState::collectionAttributesRetrieved(const Collection &collection) +{ + m_resource->collectionAttributesRetrieved(collection); +} + +void GoogleResourceState::collectionChangeCommitted(const Collection &collection) +{ + m_resource->changeCommitted(collection); +} + +void GoogleResourceState::collectionsRetrievedFromHandler(const Collection::List &collections) +{ + m_resource->collectionsRetrievedFromHandler(collections); +} + +Collection GoogleResourceState::currentCollection() +{ + return m_resource->currentCollection(); +} + +// Tags handling +void GoogleResourceState::tagsRetrieved(const Tag::List &tags, const QHash &tagMembers) +{ + m_resource->tagsRetrieved(tags, tagMembers); +} + +void GoogleResourceState::tagChangeCommitted(const Tag &tag) +{ + m_resource->changeCommitted(tag); +} + + +// Relations handling +void GoogleResourceState::relationsRetrieved(const Relation::List &relations) +{ + m_resource->relationsRetrieved(relations); +} + + +// FreeBusy handling +void GoogleResourceState::freeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText = QString()) +{ + m_resource->freeBusyRetrieved(email, freeBusy, success, errorText); +} + +void GoogleResourceState::handlesFreeBusy(const QString &email, bool handles) +{ + m_resource->handlesFreeBusy(email, handles); +} + + +// Result reporting +void GoogleResourceState::changeProcessed() +{ + m_resource->changeProcessed(); +} + +void GoogleResourceState::cancelTask(const QString &errorString) +{ + m_resource->cancelTask(errorString); +} + +void GoogleResourceState::deferTask() +{ + m_resource->deferTask(); +} + +void GoogleResourceState::taskDone() +{ + m_resource->taskDone(); +} + +void GoogleResourceState::emitStatus(int status, const QString &message) +{ + Q_EMIT m_resource->status(status, message); +} + +void GoogleResourceState::emitError(const QString &message) +{ + Q_EMIT m_resource->error(message); +} + +void GoogleResourceState::emitWarning(const QString &message) +{ + Q_EMIT m_resource->warning(message); +} + +void GoogleResourceState::emitPercent(int percent) +{ + Q_EMIT m_resource->percent(percent); +} + +bool GoogleResourceState::canPerformTask() +{ + return m_resource->canPerformTask(); +} + +bool GoogleResourceState::handleError(KGAPI2::Job *job, bool _cancelTask) +{ + return m_resource->handleError(job, _cancelTask); +} + +void GoogleResourceState::scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument) +{ + return m_resource->scheduleCustomTask(receiver, method, argument); +} diff --git a/resources/google-groupware/googleresourcestate.h b/resources/google-groupware/googleresourcestate.h new file mode 100644 index 000000000..c8d614e7f --- /dev/null +++ b/resources/google-groupware/googleresourcestate.h @@ -0,0 +1,82 @@ +/* + Copyright (c) 2020 Igor Poboiko + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef GOOGLERESOURCESTATE_H +#define GOOGLERESOURCESTATE_H + +#include "googleresourcestateinterface.h" + +class GoogleResource; + +class GoogleResourceState : public GoogleResourceStateInterface +{ +public: + explicit GoogleResourceState(GoogleResource *resource); + ~GoogleResourceState() = default; + + // Items handling + void itemRetrieved(const Akonadi::Item &item) override; + void itemsRetrieved(const Akonadi::Item::List &items) override; + void itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed) override; + void itemsRetrievalDone() override; + void setTotalItems(int) override; + void itemChangeCommitted(const Akonadi::Item &item) override; + void itemsChangesCommitted(const Akonadi::Item::List &items) override; + Akonadi::Item::List currentItems() override; + + // Collections handling + void collectionsRetrieved(const Akonadi::Collection::List &collections) override; + void collectionAttributesRetrieved(const Akonadi::Collection &collection) override; + void collectionChangeCommitted(const Akonadi::Collection &collection) override; + void collectionsRetrievedFromHandler(const Akonadi::Collection::List &collections) override; + Akonadi::Collection currentCollection() override; + + // Tags handling + void tagsRetrieved(const Akonadi::Tag::List &tags, const QHash &) override; + void tagChangeCommitted(const Akonadi::Tag &tag) override; + + // Relations handling + void relationsRetrieved(const Akonadi::Relation::List &tags) override; + + // FreeBusy handling + void freeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText) override; + void handlesFreeBusy(const QString &email, bool handles) override; + + // Result reporting + void changeProcessed() override; + void cancelTask(const QString &errorString) override; + void deferTask() override; + void taskDone() override; + + void emitStatus(int status, const QString &message) override; + void emitError(const QString &message) override; + void emitWarning(const QString &message) override; + void emitPercent(int percent) override; + + // Other + void scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument) override; + + // Google-specific stuff + bool canPerformTask() override; + bool handleError(KGAPI2::Job *job, bool _cancelTask) override; +private: + GoogleResource *m_resource = nullptr; +}; + +#endif // GOOGLERESOURCESTATE_H diff --git a/resources/google-groupware/googleresourcestateinterface.h b/resources/google-groupware/googleresourcestateinterface.h new file mode 100644 index 000000000..df63cddcb --- /dev/null +++ b/resources/google-groupware/googleresourcestateinterface.h @@ -0,0 +1,61 @@ +/* + Copyright (c) 2020 Igor Poboiko + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef GOOGLERESOURCESTATEINTERFACE_H +#define GOOGLERESOURCESTATEINTERFACE_H + +#include "resourcestateinterface.h" + +namespace KGAPI2 +{ +class Job; +} + +/** + * This is a ResourceStateInterface with some specific for Google Resource bits + */ +class GoogleResourceStateInterface : public ResourceStateInterface +{ +public: + /** + * Returns whether the resource is operational (i.e. account is configured) + */ + virtual bool canPerformTask() = 0; + + /** + * Handles an error (if any) for a job. It includes cancelling a task + * (if there was an error), or retrying to authenticate (if necessary) + */ + virtual bool handleError(KGAPI2::Job *job, bool _cancelTask = true) = 0; + + /** + * Each handler use this to report that it has finished collection fetching + */ + virtual void collectionsRetrievedFromHandler(const Akonadi::Collection::List &collections) = 0; + + /** + * FreeBusy + */ + virtual void freeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText = QString()) = 0; + virtual void handlesFreeBusy(const QString &email, bool handles) = 0; + + virtual void scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument) = 0; +}; + +#endif // GOOGLERESOURCESTATEINTERFACE_H diff --git a/resources/google-groupware/googlesettings.cpp b/resources/google-groupware/googlesettings.cpp new file mode 100644 index 000000000..9a23ce8fe --- /dev/null +++ b/resources/google-groupware/googlesettings.cpp @@ -0,0 +1,183 @@ +/* + Copyright (C) 2011-2013 Dan Vratil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "googlesettings.h" +#include "settingsbase.h" +#include "googleresource_debug.h" + +#include +#include +#include + +using namespace KWallet; +using namespace KGAPI2; + +static const QString googleWalletFolder = QStringLiteral("Akonadi Google"); + +GoogleSettings::GoogleSettings() + : m_winId(0) +{ + m_wallet = Wallet::openWallet(Wallet::NetworkWallet(), + m_winId, Wallet::Asynchronous); + if (m_wallet) { + connect(m_wallet.data(), &Wallet::walletOpened, this, &GoogleSettings::slotWalletOpened); + } else { + qCWarning(GOOGLE_LOG) << "Failed to open wallet!"; + } +} + +void GoogleSettings::slotWalletOpened(bool success) +{ + if (!success) { + qCWarning(GOOGLE_LOG) << "Failed to open wallet!"; + Q_EMIT accountReady(false); + return; + } + + if (!m_wallet->hasFolder(googleWalletFolder) + && !m_wallet->createFolder(googleWalletFolder)) { + qCWarning(GOOGLE_LOG) << "Failed to create wallet folder" << googleWalletFolder; + Q_EMIT accountReady(false); + return; + } + + if (!m_wallet->setFolder(googleWalletFolder)) { + qWarning() << "Failed to open wallet folder" << googleWalletFolder; + Q_EMIT accountReady(false); + return; + } + qCDebug(GOOGLE_LOG) << "Wallet opened, reading" << account(); + if (!account().isEmpty()) { + m_account = fetchAccountFromWallet(account()); + } + m_isReady = true; + Q_EMIT accountReady(true); +} + +KGAPI2::AccountPtr GoogleSettings::fetchAccountFromWallet(const QString &accountName) +{ + if (!m_wallet->entryList().contains(accountName)) { + qCDebug(GOOGLE_LOG) << "Account" << accountName << "not found in KWallet"; + return AccountPtr(); + } + + QMap map; + m_wallet->readMap(accountName, map); + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + const QStringList scopes = map[QStringLiteral("scopes")].split(QLatin1Char(','), QString::SkipEmptyParts); +#else + const QStringList scopes = map[QStringLiteral("scopes")].split(QLatin1Char(','), Qt::SkipEmptyParts); +#endif + QList scopeUrls; + scopeUrls.reserve(scopes.count()); + for (const QString &scope : scopes) { + scopeUrls << QUrl(scope); + } + AccountPtr account(new Account(accountName, + map[QStringLiteral("accessToken")], + map[QStringLiteral("refreshToken")], + scopeUrls)); + return account; +} + +bool GoogleSettings::storeAccount(AccountPtr account) +{ + // Removing the old one (if present) + if (m_account && (account->accountName() != m_account->accountName())) { + cleanup(); + } + // Populating the new one + m_account = account; + + QStringList scopes; + const QList urlScopes = m_account->scopes(); + scopes.reserve(urlScopes.count()); + for (const QUrl &url : urlScopes) { + scopes << url.toString(); + } + + QMap map; + map[QStringLiteral("accessToken")] = m_account->accessToken(); + map[QStringLiteral("refreshToken")] = m_account->refreshToken(); + map[QStringLiteral("scopes")] = scopes.join(QLatin1Char(',')); + // Removing previous junk (if present) + cleanup(); + if (m_wallet->writeMap(m_account->accountName(), map) != 0) { + qCWarning(GOOGLE_LOG) << "Failed to write new account entry to wallet"; + return false; + } + SettingsBase::setAccount(m_account->accountName()); + m_isReady = true; + return true; +} + +void GoogleSettings::cleanup() +{ + if (m_account && m_wallet) { + m_wallet->removeEntry(m_account->accountName()); + } +} + +void GoogleSettings::addCalendar(const QString &calendar) +{ + if (calendars().isEmpty() || calendars().contains(calendar)) { + return; + } + setCalendars(calendars() << calendar); + save(); +} + +void GoogleSettings::addTaskList(const QString &taskList) +{ + if (calendars().isEmpty() || taskLists().contains(taskList)) { + return; + } + setTaskLists(taskLists() << taskList); + save(); +} + +QString GoogleSettings::clientId() const +{ + return QStringLiteral("554041944266.apps.googleusercontent.com"); +} + +QString GoogleSettings::clientSecret() const +{ + return QStringLiteral("mdT1DjzohxN3npUUzkENT0gO"); +} + +bool GoogleSettings::isReady() const +{ + return m_isReady; +} + +AccountPtr GoogleSettings::accountPtr() +{ + return m_account; +} + +void GoogleSettings::setWindowId(WId id) +{ + m_winId = id; +} + +void GoogleSettings::setResourceId(const QString &resourceIdentificator) +{ + m_resourceId = resourceIdentificator; +} diff --git a/resources/google-groupware/googlesettings.h b/resources/google-groupware/googlesettings.h new file mode 100644 index 000000000..fa3804943 --- /dev/null +++ b/resources/google-groupware/googlesettings.h @@ -0,0 +1,77 @@ +/* + Copyright (C) 2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GOOGLESETTINGS_H +#define GOOGLESETTINGS_H + +#include "settingsbase.h" + +#include +#include + +#include + +namespace KWallet { +class Wallet; +} + +/** + * @brief Settings object + * + * Provides read-only access to application clientId and + * clientSecret and read-write access to accessToken and + * refreshToken. Interacts with KWallet. + */ +class GoogleSettings : public SettingsBase +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Google.ExtendedSettings") + +public: + GoogleSettings(); + void setWindowId(WId id); + void setResourceId(const QString &resourceIdentifier); + + QString appId() const; + QString clientId() const; + QString clientSecret() const; + + void addCalendar(const QString &calendar); + void addTaskList(const QString &taskList); + + KGAPI2::AccountPtr accountPtr(); + // Wallet + bool isReady() const; + bool storeAccount(KGAPI2::AccountPtr account); + void cleanup(); +Q_SIGNALS: + void accountReady(bool ready); + void accountChanged(); +private Q_SLOTS: + void slotWalletOpened(bool success); +private: + WId m_winId; + QString m_resourceId; + bool m_isReady = false; + KGAPI2::AccountPtr m_account; + QPointer m_wallet; + + KGAPI2::AccountPtr fetchAccountFromWallet(const QString &accountName); +}; + +#endif // GOOGLESETTINGS_H diff --git a/resources/google-groupware/googlesettingsdialog.cpp b/resources/google-groupware/googlesettingsdialog.cpp new file mode 100644 index 000000000..51414c9e5 --- /dev/null +++ b/resources/google-groupware/googlesettingsdialog.cpp @@ -0,0 +1,294 @@ +/* + Copyright (C) 2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "googlesettingsdialog.h" +#include "ui_googlesettingsdialog.h" +#include "googlesettings.h" +#include "googleresource.h" +#include "googleresource_debug.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KGAPI2; + +GoogleSettingsDialog::GoogleSettingsDialog(GoogleResource *resource, GoogleSettings *settings, WId wId) + : QDialog() + , m_resource(resource) + , m_settings(settings) +{ + if (wId) { + setAttribute(Qt::WA_NativeWindow, true); + KWindowSystem::setMainWindow(windowHandle(), wId); + } + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + QWidget *mainWidget = new QWidget(this); + mainLayout->addWidget(mainWidget); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); + mainLayout->addWidget(buttonBox); + + m_ui = new Ui::GoogleSettingsDialog; + m_ui->setupUi(mainWidget); + + m_ui->refreshSpinBox->setSuffix(ki18np(" minute", " minutes")); + m_ui->enableRefresh->setChecked(m_settings->enableIntervalCheck()); + m_ui->refreshSpinBox->setEnabled(m_settings->enableIntervalCheck()); + if (m_settings->enableIntervalCheck()) { + m_ui->refreshSpinBox->setValue(m_settings->intervalCheckTime()); + } else { + m_ui->refreshSpinBox->setValue(30); + } + + m_ui->eventsLimitCombo->setMaximumDate(QDate::currentDate()); + m_ui->eventsLimitCombo->setMinimumDate(QDate::fromString(QStringLiteral("2000-01-01"), Qt::ISODate)); + m_ui->eventsLimitCombo->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate + |KDateComboBox::DatePicker | KDateComboBox::WarnOnInvalid); + if (m_settings->eventsSince().isEmpty()) { + const QString ds = QStringLiteral("%1-01-01").arg(QString::number(QDate::currentDate().year() - 3)); + m_ui->eventsLimitCombo->setDate(QDate::fromString(ds, Qt::ISODate)); + } else { + m_ui->eventsLimitCombo->setDate(QDate::fromString(m_settings->eventsSince(), Qt::ISODate)); + } + + connect(buttonBox, &QDialogButtonBox::accepted, this, &GoogleSettingsDialog::slotSaveSettings); + connect(buttonBox, &QDialogButtonBox::rejected, this, &GoogleSettingsDialog::reject); + connect(m_ui->reloadCalendarsBtn, &QPushButton::clicked, this, &GoogleSettingsDialog::slotReloadCalendars); + connect(m_ui->reloadTaskListsBtn, &QPushButton::clicked, this, &GoogleSettingsDialog::slotReloadTaskLists); + connect(m_ui->configureBtn, &QPushButton::clicked, this, &GoogleSettingsDialog::slotConfigure); + if (m_settings->isReady()) { + m_account = m_settings->accountPtr(); + } + connect(m_settings, &GoogleSettings::accountReady, this, [this](bool ready){ + if (ready) { + m_account = m_settings->accountPtr(); + accountChanged(); + } + }); + QMetaObject::invokeMethod(this, &GoogleSettingsDialog::accountChanged, Qt::QueuedConnection); +} + +GoogleSettingsDialog::~GoogleSettingsDialog() +{ + delete m_ui; +} + +bool GoogleSettingsDialog::handleError(Job *job) +{ + if ((job->error() == KGAPI2::NoError) || (job->error() == KGAPI2::OK)) { + return true; + } + + if (job->error() == KGAPI2::Unauthorized) { + qCDebug(GOOGLE_LOG) << job << job->errorString(); + const QList resourceScopes = m_resource->scopes(); + for (const QUrl &scope : resourceScopes) { + if (!m_account->scopes().contains(scope)) { + m_account->addScope(scope); + } + } + + AuthJob *authJob = new AuthJob(m_account, m_settings->clientId(), + m_settings->clientSecret(), this); + authJob->setProperty(JOB_PROPERTY, QVariant::fromValue(job)); + connect(authJob, &AuthJob::finished, this, &GoogleSettingsDialog::slotAuthJobFinished); + + return false; + } + + KMessageBox::sorry(this, job->errorString()); + return false; +} + +void GoogleSettingsDialog::accountChanged() +{ + if (!m_account) { + m_ui->accountLabel->setText(i18n("not configured")); + m_ui->calendarsBox->setDisabled(true); + m_ui->calendarsList->clear(); + m_ui->taskListsBox->setDisabled(true); + m_ui->taskListsList->clear(); + return; + } + m_ui->accountLabel->setText(QStringLiteral("%1").arg(m_account->accountName())); + slotReloadCalendars(); + slotReloadTaskLists(); +} + +void GoogleSettingsDialog::slotConfigure() +{ + m_account = AccountPtr(new Account()); + const QList resourceScopes = m_resource->scopes(); + for (const QUrl &scope : resourceScopes) { + if (!m_account->scopes().contains(scope)) { + m_account->addScope(scope); + } + } + AuthJob *authJob = new AuthJob(m_account, + m_settings->clientId(), + m_settings->clientSecret()); + connect(authJob, &AuthJob::finished, this, &GoogleSettingsDialog::slotAuthJobFinished); +} + +void GoogleSettingsDialog::slotAuthJobFinished(Job *job) +{ + auto authJob = qobject_cast(job); + m_account = authJob->account(); + if (authJob->error() != KGAPI2::NoError) { + KMessageBox::sorry(this, authJob->errorString()); + return; + } + accountChanged(); + + auto otherJob = job->property(JOB_PROPERTY).value(); + if (otherJob) { + otherJob->setAccount(m_account); + otherJob->restart(); + } +} + +void GoogleSettingsDialog::slotSaveSettings() +{ + if (!m_account || !m_settings->storeAccount(m_account)) { + m_settings->setAccount(QString()); + m_settings->setEnableIntervalCheck(m_ui->enableRefresh->isChecked()); + m_settings->setIntervalCheckTime(m_ui->refreshSpinBox->value()); + m_settings->setCalendars({}); + m_settings->setTaskLists({}); + m_settings->setEventsSince(QString()); + m_settings->save(); + return; + } + m_settings->setAccount(m_account->accountName()); + m_settings->setEnableIntervalCheck(m_ui->enableRefresh->isChecked()); + m_settings->setIntervalCheckTime(m_ui->refreshSpinBox->value()); + + QStringList calendars; + for (int i = 0; i < m_ui->calendarsList->count(); i++) { + QListWidgetItem *item = m_ui->calendarsList->item(i); + + if (item->checkState() == Qt::Checked) { + calendars.append(item->data(Qt::UserRole).toString()); + } + } + m_settings->setCalendars(calendars); + + if (m_ui->eventsLimitCombo->isValid()) { + m_settings->setEventsSince(m_ui->eventsLimitCombo->date().toString(Qt::ISODate)); + } + + QStringList taskLists; + for (int i = 0; i < m_ui->taskListsList->count(); i++) { + QListWidgetItem *item = m_ui->taskListsList->item(i); + + if (item->checkState() == Qt::Checked) { + taskLists.append(item->data(Qt::UserRole).toString()); + } + } + m_settings->setTaskLists(taskLists); + m_settings->save(); + + accept(); +} + +void GoogleSettingsDialog::slotReloadCalendars() +{ + m_ui->calendarsBox->setDisabled(true); + m_ui->calendarsList->clear(); + + if (!m_account) { + return; + } + + auto fetchJob = new CalendarFetchJob(m_account, this); + connect(fetchJob, &CalendarFetchJob::finished, this, [this](Job *job){ + if (!handleError(job) || !m_account) { + m_ui->calendarsBox->setEnabled(false); + return; + } + + const ObjectsList objects = qobject_cast(job)->items(); + + QStringList activeCalendars; + if (m_account->accountName() == m_settings->account()) { + activeCalendars = m_settings->calendars(); + } + m_ui->calendarsList->clear(); + for (const ObjectPtr &object : objects) { + const CalendarPtr calendar = object.dynamicCast(); + + QListWidgetItem *item = new QListWidgetItem(calendar->title()); + item->setData(Qt::UserRole, calendar->uid()); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable); + item->setCheckState((activeCalendars.isEmpty() || activeCalendars.contains(calendar->uid())) ? Qt::Checked : Qt::Unchecked); + m_ui->calendarsList->addItem(item); + } + + m_ui->calendarsBox->setEnabled(true); + }); +} + +void GoogleSettingsDialog::slotReloadTaskLists() +{ + if (!m_account) { + return; + } + + m_ui->taskListsBox->setDisabled(true); + m_ui->taskListsList->clear(); + + auto job = new TaskListFetchJob(m_account, this); + connect(job, &TaskListFetchJob::finished, this, [this](KGAPI2::Job *job){ + if (!handleError(job) || !m_account) { + m_ui->taskListsBox->setDisabled(true); + return; + } + + const ObjectsList objects = qobject_cast(job)->items(); + + QStringList activeTaskLists; + if (m_account->accountName() == m_settings->account()) { + activeTaskLists = m_settings->taskLists(); + } + m_ui->taskListsList->clear(); + for (const ObjectPtr &object : objects) { + const TaskListPtr taskList = object.dynamicCast(); + + QListWidgetItem *item = new QListWidgetItem(taskList->title()); + item->setData(Qt::UserRole, taskList->uid()); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable); + item->setCheckState((activeTaskLists.isEmpty() || activeTaskLists.contains(taskList->uid())) ? Qt::Checked : Qt::Unchecked); + m_ui->taskListsList->addItem(item); + } + + m_ui->taskListsBox->setEnabled(true); + + }); +} diff --git a/resources/google-groupware/googlesettingsdialog.h b/resources/google-groupware/googlesettingsdialog.h new file mode 100644 index 000000000..35db7163f --- /dev/null +++ b/resources/google-groupware/googlesettingsdialog.h @@ -0,0 +1,56 @@ +/* + Copyright (C) 2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GOOGLESETTINGSDIALOG_H +#define GOOGLESETTINGSDIALOG_H + +#include +#include + +namespace Ui { + class GoogleSettingsDialog; +} +namespace KGAPI2 { + class Job; +} +class GoogleResource; +class GoogleSettings; + +class GoogleSettingsDialog : public QDialog +{ + Q_OBJECT +public: + explicit GoogleSettingsDialog(GoogleResource *resource, GoogleSettings *settings, WId wId); + ~GoogleSettingsDialog(); +protected: + bool handleError(KGAPI2::Job *job); + void accountChanged(); +private: + GoogleResource *m_resource; + GoogleSettings *m_settings; + Ui::GoogleSettingsDialog *m_ui = nullptr; + KGAPI2::AccountPtr m_account; +private Q_SLOTS: + void slotConfigure(); + void slotAuthJobFinished(KGAPI2::Job *job); + void slotSaveSettings(); + void slotReloadCalendars(); + void slotReloadTaskLists(); +}; + +#endif // GOOGLESETTINGSDIALOG_H diff --git a/resources/google-groupware/googlesettingsdialog.ui b/resources/google-groupware/googlesettingsdialog.ui new file mode 100644 index 000000000..832a32d7c --- /dev/null +++ b/resources/google-groupware/googlesettingsdialog.ui @@ -0,0 +1,182 @@ + + + GoogleSettingsDialog + + + + 0 + 0 + 584 + 680 + + + + + + + + + Account: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>not confgured</b> + + + + + + + Configure... + + + + + + + + + Refresh + + + false + + + + + + Refresh interval: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + refreshSpinBox + + + + + + + 10 + + + 720 + + + 30 + + + + + + + Enable interval refresh + + + + + + + + + + Calendars + + + + + + + + + Fetch only events since + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + eventsLimitCombo + + + + + + + + + + Reload + + + + .. + + + + + + + + + + Tasklists + + + + + + + + + Reload + + + + .. + + + + + + + + + + + KPluralHandlingSpinBox + QSpinBox +
kpluralhandlingspinbox.h
+
+ + KDateComboBox + QComboBox +
kdatecombobox.h
+
+
+ + + + enableRefresh + toggled(bool) + refreshSpinBox + setEnabled(bool) + + + 182 + 99 + + + 312 + 128 + + + + +
diff --git a/resources/google-groupware/resourcestateinterface.h b/resources/google-groupware/resourcestateinterface.h new file mode 100644 index 000000000..81e6e862f --- /dev/null +++ b/resources/google-groupware/resourcestateinterface.h @@ -0,0 +1,77 @@ +/* + Copyright (c) 2020 Igor Poboiko + Copyright (c) 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company + Author: Kevin Ottens + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef RESOURCESTATEINTERFACE_H +#define RESOURCESTATEINTERFACE_H + +#include +#include +#include + +/** + * This is a generic interface for ResourceBase class + */ +class ResourceStateInterface +{ +public: + typedef QSharedPointer Ptr; + + virtual ~ResourceStateInterface() + { + } + + // Items handling + virtual void itemRetrieved(const Akonadi::Item &item) = 0; + virtual void itemsRetrieved(const Akonadi::Item::List &items) = 0; + virtual void itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed) = 0; + virtual void itemsRetrievalDone() = 0; + virtual void setTotalItems(int) = 0; + virtual void itemChangeCommitted(const Akonadi::Item &item) = 0; + virtual void itemsChangesCommitted(const Akonadi::Item::List &items) = 0; + virtual Akonadi::Item::List currentItems() = 0; + + // Collections handling + virtual void collectionsRetrieved(const Akonadi::Collection::List &collections) = 0; + virtual void collectionAttributesRetrieved(const Akonadi::Collection &collection) = 0; + virtual void collectionChangeCommitted(const Akonadi::Collection &collection) = 0; + virtual Akonadi::Collection currentCollection() = 0; + + // Tags handling + virtual void tagsRetrieved(const Akonadi::Tag::List &tags, const QHash &) = 0; + virtual void tagChangeCommitted(const Akonadi::Tag &tag) = 0; + + // Relations handling + virtual void relationsRetrieved(const Akonadi::Relation::List &tags) = 0; + + // Result reporting + virtual void changeProcessed() = 0; + virtual void cancelTask(const QString &errorString) = 0; + virtual void deferTask() = 0; + virtual void taskDone() = 0; + + virtual void emitStatus(int status, const QString &message) = 0; + virtual void emitError(const QString &message) = 0; + virtual void emitWarning(const QString &message) = 0; + virtual void emitPercent(int percent) = 0; +}; + +#endif // RESOURCESTATEINTERFACE_H diff --git a/resources/google-groupware/settingsbase.kcfg b/resources/google-groupware/settingsbase.kcfg new file mode 100644 index 000000000..ac58395d7 --- /dev/null +++ b/resources/google-groupware/settingsbase.kcfg @@ -0,0 +1,37 @@ + + + + + + + + + + + 0 + + + + + + + + + + + + + + + false + + + 60 + + + diff --git a/resources/google-groupware/settingsbase.kcfgc b/resources/google-groupware/settingsbase.kcfgc new file mode 100644 index 000000000..e112e9f8a --- /dev/null +++ b/resources/google-groupware/settingsbase.kcfgc @@ -0,0 +1,6 @@ +File=settingsbase.kcfg +ClassName=SettingsBase +Mutators=true +ItemAccessors=true +Singleton=false +GlobalEnums=true diff --git a/resources/google-groupware/taskhandler.cpp b/resources/google-groupware/taskhandler.cpp new file mode 100644 index 000000000..e1b65e08b --- /dev/null +++ b/resources/google-groupware/taskhandler.cpp @@ -0,0 +1,357 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Poboiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "taskhandler.h" +#include "googleresource.h" +#include "googlesettings.h" +#include "googletasks_debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define TASK_PROPERTY "_KGAPI2::TaskPtr" + +using namespace KGAPI2; +using namespace Akonadi; + +QString TaskHandler::mimeType() +{ + return KCalendarCore::Todo::todoMimeType(); +} + +bool TaskHandler::canPerformTask(const Item &item) +{ + return GenericHandler::canPerformTask(item); +} + +bool TaskHandler::canPerformTask(const Item::List &items) +{ + return GenericHandler::canPerformTask(items); +} + +void TaskHandler::setupCollection(Collection &collection, const TaskListPtr &taskList) +{ + collection.setContentMimeTypes({ mimeType() }); + collection.setName(taskList->uid()); + collection.setRemoteId(taskList->uid()); + collection.setRights(Collection::CanChangeCollection + |Collection::CanCreateItem + |Collection::CanChangeItem + |Collection::CanDeleteItem); + + EntityDisplayAttribute *attr = collection.attribute(Collection::AddIfMissing); + attr->setDisplayName(taskList->title()); + attr->setIconName(QStringLiteral("view-pim-tasks")); +} + +void TaskHandler::retrieveCollections(const Collection &rootCollection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving task lists")); + qCDebug(GOOGLE_TASKS_LOG) << "Retrieving tasks..."; + auto job = new TaskListFetchJob(m_settings->accountPtr(), this); + connect(job, &TaskListFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + qCDebug(GOOGLE_TASKS_LOG) << "Task lists retrieved"; + + const ObjectsList taskLists = qobject_cast(job)->items(); + const QStringList activeTaskLists = m_settings->taskLists(); + Collection::List collections; + for (const ObjectPtr &object : taskLists) { + const TaskListPtr &taskList = object.dynamicCast(); + qCDebug(GOOGLE_TASKS_LOG) << " -" << taskList->title() << "(" << taskList->uid() << ")"; + + if (!activeTaskLists.contains(taskList->uid())) { + qCDebug(GOOGLE_TASKS_LOG) << "Skipping, not subscribed"; + continue; + } + + Collection collection; + setupCollection(collection, taskList); + collection.setParentCollection(rootCollection); + collections << collection; + } + + m_iface->collectionsRetrievedFromHandler(collections); + }); +} + +void TaskHandler::retrieveItems(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving tasks for list '%1'", collection.displayName())); + qCDebug(GOOGLE_TASKS_LOG) << "Retrieving tasks for list" << collection.remoteId(); + // https://bugs.kde.org/show_bug.cgi?id=308122: we can only request changes in + // max. last 25 days, otherwise we get an error. + int lastSyncDelta = -1; + if (!collection.remoteRevision().isEmpty()) { + lastSyncDelta = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - collection.remoteRevision().toULongLong(); + } + + auto job = new TaskFetchJob(collection.remoteId(), m_settings->accountPtr(), this); + if (lastSyncDelta > -1 && lastSyncDelta < 25 * 25 * 3600) { + job->setFetchOnlyUpdated(collection.remoteRevision().toULongLong()); + job->setFetchDeleted(true); + } else { + // No need to fetch deleted items for non-incremental update + job->setFetchDeleted(false); + } + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &TaskFetchJob::finished, this, &TaskHandler::slotItemsRetrieved); +} + +void TaskHandler::slotItemsRetrieved(KGAPI2::Job *job) +{ + if (!m_iface->handleError(job)) { + return; + } + Item::List changedItems, removedItems; + + const ObjectsList &objects = qobject_cast(job)->items(); + Collection collection = job->property(COLLECTION_PROPERTY).value(); + bool isIncremental = (qobject_cast(job)->fetchOnlyUpdated() > 0); + qCDebug(GOOGLE_TASKS_LOG) << "Retrieved" << objects.count() << "tasks for list" << collection.remoteId(); + for (const auto &object : objects) { + const TaskPtr task = object.dynamicCast(); + + Item item; + item.setMimeType( mimeType() ); + item.setParentCollection(collection); + item.setRemoteId(task->uid()); + item.setRemoteRevision(task->etag()); + item.setPayload(task.dynamicCast()); + + if (task->deleted()) { + qCDebug(GOOGLE_TASKS_LOG) << " - removed" << task->uid(); + removedItems << item; + } else { + qCDebug(GOOGLE_TASKS_LOG) << " - changed" << task->uid(); + changedItems << item; + } + } + + if (isIncremental) { + m_iface->itemsRetrievedIncremental(changedItems, removedItems); + } else { + m_iface->itemsRetrieved(changedItems); + } + const QDateTime local(QDateTime::currentDateTime()); + const QDateTime UTC(local.toUTC()); + + collection.setRemoteRevision(QString::number(UTC.toSecsSinceEpoch())); + new CollectionModifyJob(collection, this); + + emitReadyStatus(); +} + +void TaskHandler::itemAdded(const Item &item, const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding event to calendar '%1'", collection.displayName())); + KCalendarCore::Todo::Ptr todo = item.payload(); + TaskPtr task(new Task(*todo)); + const QString parentRemoteId = task->relatedTo(KCalendarCore::Incidence::RelTypeParent); + qCDebug(GOOGLE_TASKS_LOG) << "Task added to list" << collection.remoteId() << "with parent" << parentRemoteId; + auto job = new TaskCreateJob(task, item.parentCollection().remoteId(), m_settings->accountPtr(), this); + job->setParentItem(parentRemoteId); + connect(job, &TaskCreateJob::finished, this, [this, item](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + Item newItem = item; + const TaskPtr task = qobject_cast(job)->items().first().dynamicCast(); + qCDebug(GOOGLE_TASKS_LOG) << "Task added"; + newItem.setRemoteId(task->uid()); + newItem.setRemoteRevision(task->etag()); + newItem.setGid(task->uid()); + m_iface->itemChangeCommitted(newItem); + newItem.setPayload(task.dynamicCast()); + new ItemModifyJob(newItem, this); + emitReadyStatus(); + }); +} + +void TaskHandler::itemChanged(const Item &item, const QSet< QByteArray > &/*partIdentifiers*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing task in list '%1'", item.parentCollection().displayName())); + qCDebug(GOOGLE_TASKS_LOG) << "Changing task" << item.remoteId(); + + KCalendarCore::Todo::Ptr todo = item.payload(); + const QString parentUid = todo->relatedTo(KCalendarCore::Incidence::RelTypeParent); + // First we move it to a new parent, if there is + auto job = new TaskMoveJob(item.remoteId(), item.parentCollection().remoteId(), parentUid, m_settings->accountPtr(), this); + connect(job, &TaskMoveJob::finished, this, [this, todo, item](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + TaskPtr task(new Task(*todo)); + auto newJob = new TaskModifyJob(task, item.parentCollection().remoteId(), job->account(), this); + newJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); + connect(newJob, &TaskModifyJob::finished, this, &TaskHandler::slotGenericJobFinished); + }); +} + +void TaskHandler::itemsRemoved(const Item::List &items) +{ + m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 tasks", "Removing %1 task", items.count())); + qCDebug(GOOGLE_TASKS_LOG) << "Removing" << items.count() << "tasks"; + /* Google always automatically removes tasks with all their subtasks. In KOrganizer + * by default we only remove the item we are given. For this reason we have to first + * fetch all tasks, find all sub-tasks for the task being removed and detach them + * from the task. Only then the task can be safely removed. */ + auto job = new ItemFetchJob(items.first().parentCollection()); + job->fetchScope().fetchFullPayload(true); + connect(job, &ItemFetchJob::finished, this, [this, items](KJob *job){ + if (job->error()) { + m_iface->cancelTask(i18n("Failed to delete task: %1", job->errorString())); + return; + } + const Item::List fetchedItems = qobject_cast(job)->items(); + Item::List detachItems; + TasksList detachTasks; + for (const Item &fetchedItem : fetchedItems) { + auto todo = fetchedItem.payload(); + TaskPtr task(new Task(*todo)); + const QString parentId = task->relatedTo(KCalendarCore::Incidence::RelTypeParent); + if (parentId.isEmpty()) { + continue; + } + + auto it = std::find_if(items.cbegin(), items.cend(), [&parentId](const Item &item){ + return item.remoteId() == parentId; + }); + if (it != items.cend()) { + Item newItem(fetchedItem); + qCDebug(GOOGLE_TASKS_LOG) << "Detaching child" << newItem.remoteId() << "from" << parentId; + todo->setRelatedTo(QString(), KCalendarCore::Incidence::RelTypeParent); + newItem.setPayload(todo); + detachItems << newItem; + detachTasks << task; + } + } + /* If there are no items do detach, then delete the task right now */ + if (detachItems.isEmpty()) { + doRemoveTasks(items); + return; + } + + qCDebug(GOOGLE_TASKS_LOG) << "Reparenting" << detachItems.count() << "children..."; + auto moveJob = new TaskMoveJob(detachTasks, items.first().parentCollection().remoteId(), + QString(), m_settings->accountPtr(), this); + connect(moveJob, &TaskMoveJob::finished, this, [this, items, detachItems](KGAPI2::Job *job){ + if (job->error()) { + m_iface->cancelTask(i18n("Failed to reparent subtasks: %1", job->errorString())); + return; + } + // Update items inside Akonadi DB too + new ItemModifyJob(detachItems); + // Perform actual removal + doRemoveTasks(items); + }); + }); +} + +void TaskHandler::doRemoveTasks(const Item::List &items) +{ + // Make sure account is still valid + if (!m_iface->canPerformTask()) { + return; + } + QStringList taskIds; + taskIds.reserve(items.count()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(taskIds), + [](const Item &item){ + return item.remoteId(); + }); + + /* Now finally we can safely remove the task we wanted to */ + auto job = new TaskDeleteJob(taskIds, items.first().parentCollection().remoteId(), m_settings->accountPtr(), this); + job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); + connect(job, &TaskDeleteJob::finished, this, &TaskHandler::slotGenericJobFinished); +} + +void TaskHandler::itemsMoved(const Item::List &/*item*/, const Collection &/*collectionSource*/, const Collection &/*collectionDestination*/) +{ + m_iface->cancelTask(i18n("Moving tasks between task lists is not supported")); +} + +void TaskHandler::collectionAdded(const Collection &collection, const Collection &/*parent*/) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating new task list '%1'", collection.displayName())); + qCDebug(GOOGLE_TASKS_LOG) << "Adding task list" << collection.displayName(); + TaskListPtr taskList(new TaskList()); + taskList->setTitle(collection.displayName()); + + auto job = new TaskListCreateJob(taskList, m_settings->accountPtr(), this); + connect(job, &TaskListCreateJob::finished, this, [this, collection](KGAPI2::Job *job){ + if (!m_iface->handleError(job)) { + return; + } + + TaskListPtr taskList = qobject_cast(job)->items().first().dynamicCast(); + qCDebug(GOOGLE_TASKS_LOG) << "Task list created:" << taskList->uid(); + // Enable newly added task list in settings + m_settings->addTaskList(taskList->uid()); + // Populate remoteId & other stuff + Collection newCollection(collection); + setupCollection(newCollection, taskList); + m_iface->collectionChangeCommitted(newCollection); + emitReadyStatus(); + }); +} + +void TaskHandler::collectionChanged(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing task list '%1'", collection.displayName())); + qCDebug(GOOGLE_TASKS_LOG) << "Changing task list" << collection.remoteId(); + + TaskListPtr taskList(new TaskList()); + taskList->setUid(collection.remoteId()); + taskList->setTitle(collection.displayName()); + auto job = new TaskListModifyJob(taskList, m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &TaskListModifyJob::finished, this, &TaskHandler::slotGenericJobFinished); +} + +void TaskHandler::collectionRemoved(const Collection &collection) +{ + m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing task list '%1'", collection.displayName())); + qCDebug(GOOGLE_TASKS_LOG) << "Removing task list" << collection.remoteId(); + auto job = new TaskListDeleteJob(collection.remoteId(), m_settings->accountPtr(), this); + job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); + connect(job, &TaskListDeleteJob::finished, this, &TaskHandler::slotGenericJobFinished); +} diff --git a/resources/google-groupware/taskhandler.h b/resources/google-groupware/taskhandler.h new file mode 100644 index 000000000..a4bf4fe07 --- /dev/null +++ b/resources/google-groupware/taskhandler.h @@ -0,0 +1,53 @@ +/* + Copyright (C) 2011-2013 Daniel Vrátil + 2020 Igor Pobiko + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef TASKHANDLER_H +#define TASKHANDLER_H + +#include "generichandler.h" +#include + + +class TaskHandler : public GenericHandler +{ +public: + using GenericHandler::GenericHandler; + + QString mimeType() override; + bool canPerformTask(const Akonadi::Item &item) override; + bool canPerformTask(const Akonadi::Item::List &items) override; + + void retrieveCollections(const Akonadi::Collection &rootCollection) override; + void retrieveItems(const Akonadi::Collection &collection) override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) override; + void itemsRemoved(const Akonadi::Item::List &items) override; + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) override; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + void collectionChanged(const Akonadi::Collection &collection) override; + void collectionRemoved(const Akonadi::Collection &collection) override; +private Q_SLOTS: + void slotItemsRetrieved(KGAPI2::Job *job); +private: + void setupCollection(Akonadi::Collection &colleciton, const KGAPI2::TaskListPtr &taskList); + void doRemoveTasks(const Akonadi::Item::List &items); +}; + +#endif // TASKHANDLER_H