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

0001 /*
0002     SPDX-FileCopyrightText: 2011-2013 Daniel Vrátil <dvratil@redhat.com>
0003     SPDX-FileCopyrightText: 2020 Igor Poboiko <igor.poboiko@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-3.0-or-later
0006 */
0007 
0008 #include "taskhandler.h"
0009 #include "googleresource.h"
0010 #include "googlesettings.h"
0011 #include "googletasks_debug.h"
0012 
0013 #include <Akonadi/BlockAlarmsAttribute>
0014 #include <Akonadi/CollectionColorAttribute>
0015 #include <Akonadi/CollectionModifyJob>
0016 #include <Akonadi/EntityDisplayAttribute>
0017 #include <Akonadi/ItemFetchJob>
0018 #include <Akonadi/ItemFetchScope>
0019 #include <Akonadi/ItemModifyJob>
0020 
0021 #include <KGAPI/Account>
0022 #include <KGAPI/Tasks/Task>
0023 #include <KGAPI/Tasks/TaskCreateJob>
0024 #include <KGAPI/Tasks/TaskDeleteJob>
0025 #include <KGAPI/Tasks/TaskFetchJob>
0026 #include <KGAPI/Tasks/TaskList>
0027 #include <KGAPI/Tasks/TaskListCreateJob>
0028 #include <KGAPI/Tasks/TaskListDeleteJob>
0029 #include <KGAPI/Tasks/TaskListFetchJob>
0030 #include <KGAPI/Tasks/TaskListModifyJob>
0031 #include <KGAPI/Tasks/TaskModifyJob>
0032 #include <KGAPI/Tasks/TaskMoveJob>
0033 
0034 #include <KCalendarCore/Todo>
0035 
0036 #define TASK_PROPERTY "_KGAPI2::TaskPtr"
0037 
0038 using namespace KGAPI2;
0039 using namespace Akonadi;
0040 
0041 QString TaskHandler::mimeType()
0042 {
0043     return KCalendarCore::Todo::todoMimeType();
0044 }
0045 
0046 bool TaskHandler::canPerformTask(const Item &item)
0047 {
0048     return GenericHandler::canPerformTask<KCalendarCore::Todo::Ptr>(item);
0049 }
0050 
0051 bool TaskHandler::canPerformTask(const Item::List &items)
0052 {
0053     return GenericHandler::canPerformTask<KCalendarCore::Todo::Ptr>(items);
0054 }
0055 
0056 void TaskHandler::setupCollection(Collection &collection, const TaskListPtr &taskList)
0057 {
0058     collection.setContentMimeTypes({mimeType()});
0059     collection.setName(taskList->uid());
0060     collection.setRemoteId(taskList->uid());
0061     collection.setRights(Collection::CanChangeCollection | Collection::CanCreateItem | Collection::CanChangeItem | Collection::CanDeleteItem);
0062 
0063     auto attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
0064     attr->setDisplayName(taskList->title());
0065     attr->setIconName(QStringLiteral("view-pim-tasks"));
0066 }
0067 
0068 void TaskHandler::retrieveCollections(const Collection &rootCollection)
0069 {
0070     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving task lists"));
0071     qCDebug(GOOGLE_TASKS_LOG) << "Retrieving tasks...";
0072     auto job = new TaskListFetchJob(m_settings->accountPtr(), this);
0073     connect(job, &TaskListFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job) {
0074         if (!m_iface->handleError(job)) {
0075             return;
0076         }
0077         qCDebug(GOOGLE_TASKS_LOG) << "Task lists retrieved";
0078 
0079         const ObjectsList taskLists = qobject_cast<TaskListFetchJob *>(job)->items();
0080         const QStringList activeTaskLists = m_settings->taskLists();
0081         Collection::List collections;
0082         for (const ObjectPtr &object : taskLists) {
0083             const TaskListPtr &taskList = object.dynamicCast<TaskList>();
0084             qCDebug(GOOGLE_TASKS_LOG) << " -" << taskList->title() << "(" << taskList->uid() << ")";
0085 
0086             if (!activeTaskLists.contains(taskList->uid())) {
0087                 qCDebug(GOOGLE_TASKS_LOG) << "Skipping, not subscribed";
0088                 continue;
0089             }
0090 
0091             Collection collection;
0092             setupCollection(collection, taskList);
0093             collection.setParentCollection(rootCollection);
0094             collections << collection;
0095         }
0096 
0097         m_iface->collectionsRetrievedFromHandler(collections);
0098     });
0099 }
0100 
0101 void TaskHandler::retrieveItems(const Collection &collection)
0102 {
0103     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving tasks for list '%1'", collection.displayName()));
0104     qCDebug(GOOGLE_TASKS_LOG) << "Retrieving tasks for list" << collection.remoteId();
0105     // https://bugs.kde.org/show_bug.cgi?id=308122: we can only request changes in
0106     // max. last 25 days, otherwise we get an error.
0107     int lastSyncDelta = -1;
0108     if (!collection.remoteRevision().isEmpty()) {
0109         lastSyncDelta = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - collection.remoteRevision().toULongLong();
0110     }
0111 
0112     auto job = new TaskFetchJob(collection.remoteId(), m_settings->accountPtr(), this);
0113     if (lastSyncDelta > -1 && lastSyncDelta < 25 * 25 * 3600) {
0114         job->setFetchOnlyUpdated(collection.remoteRevision().toULongLong());
0115         job->setFetchDeleted(true);
0116     } else {
0117         // No need to fetch deleted items for non-incremental update
0118         job->setFetchDeleted(false);
0119     }
0120     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0121     connect(job, &TaskFetchJob::finished, this, &TaskHandler::slotItemsRetrieved);
0122 }
0123 
0124 void TaskHandler::slotItemsRetrieved(KGAPI2::Job *job)
0125 {
0126     if (!m_iface->handleError(job)) {
0127         return;
0128     }
0129     Item::List changedItems, removedItems;
0130 
0131     const ObjectsList &objects = qobject_cast<FetchJob *>(job)->items();
0132     auto collection = job->property(COLLECTION_PROPERTY).value<Collection>();
0133     bool isIncremental = (qobject_cast<TaskFetchJob *>(job)->fetchOnlyUpdated() > 0);
0134     qCDebug(GOOGLE_TASKS_LOG) << "Retrieved" << objects.count() << "tasks for list" << collection.remoteId();
0135     for (const auto &object : objects) {
0136         const TaskPtr task = object.dynamicCast<Task>();
0137 
0138         Item item;
0139         item.setMimeType(mimeType());
0140         item.setParentCollection(collection);
0141         item.setRemoteId(task->uid());
0142         item.setRemoteRevision(task->etag());
0143         item.setPayload<KCalendarCore::Todo::Ptr>(task.dynamicCast<KCalendarCore::Todo>());
0144 
0145         if (task->deleted()) {
0146             qCDebug(GOOGLE_TASKS_LOG) << " - removed" << task->uid();
0147             removedItems << item;
0148         } else {
0149             qCDebug(GOOGLE_TASKS_LOG) << " - changed" << task->uid();
0150             changedItems << item;
0151         }
0152     }
0153 
0154     if (isIncremental) {
0155         m_iface->itemsRetrievedIncremental(changedItems, removedItems);
0156     } else {
0157         m_iface->itemsRetrieved(changedItems);
0158     }
0159     const QDateTime local(QDateTime::currentDateTime());
0160     const QDateTime UTC(local.toUTC());
0161 
0162     collection.setRemoteRevision(QString::number(UTC.toSecsSinceEpoch()));
0163     new CollectionModifyJob(collection, this);
0164 
0165     emitReadyStatus();
0166 }
0167 
0168 void TaskHandler::itemAdded(const Item &item, const Collection &collection)
0169 {
0170     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding event to calendar '%1'", collection.displayName()));
0171     TaskPtr task(new Task(*item.payload<KCalendarCore::Todo::Ptr>()));
0172     const QString parentRemoteId = task->relatedTo(KCalendarCore::Incidence::RelTypeParent);
0173     qCDebug(GOOGLE_TASKS_LOG) << "Task added to list" << collection.remoteId() << "with parent" << parentRemoteId;
0174     auto job = new TaskCreateJob(task, item.parentCollection().remoteId(), m_settings->accountPtr(), this);
0175     job->setParentItem(parentRemoteId);
0176     connect(job, &TaskCreateJob::finished, this, [this, item](KGAPI2::Job *job) {
0177         if (!m_iface->handleError(job)) {
0178             return;
0179         }
0180         Item newItem = item;
0181         const TaskPtr task = qobject_cast<TaskCreateJob *>(job)->items().first().dynamicCast<Task>();
0182         qCDebug(GOOGLE_TASKS_LOG) << "Task added";
0183         newItem.setRemoteId(task->uid());
0184         newItem.setRemoteRevision(task->etag());
0185         newItem.setGid(task->uid());
0186         m_iface->itemChangeCommitted(newItem);
0187         newItem.setPayload<KCalendarCore::Todo::Ptr>(task.dynamicCast<KCalendarCore::Todo>());
0188         new ItemModifyJob(newItem, this);
0189         emitReadyStatus();
0190     });
0191 }
0192 
0193 void TaskHandler::itemChanged(const Item &item, const QSet<QByteArray> & /*partIdentifiers*/)
0194 {
0195     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing task in list '%1'", item.parentCollection().displayName()));
0196     qCDebug(GOOGLE_TASKS_LOG) << "Changing task" << item.remoteId();
0197 
0198     auto todo = item.payload<KCalendarCore::Todo::Ptr>();
0199     const QString parentUid = todo->relatedTo(KCalendarCore::Incidence::RelTypeParent);
0200     // First we move it to a new parent, if there is
0201     auto job = new TaskMoveJob(item.remoteId(), item.parentCollection().remoteId(), parentUid, m_settings->accountPtr(), this);
0202     connect(job, &TaskMoveJob::finished, this, [this, todo, item](KGAPI2::Job *job) {
0203         if (!m_iface->handleError(job)) {
0204             return;
0205         }
0206         TaskPtr task(new Task(*todo));
0207         auto newJob = new TaskModifyJob(task, item.parentCollection().remoteId(), job->account(), this);
0208         newJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
0209         connect(newJob, &TaskModifyJob::finished, this, &TaskHandler::slotGenericJobFinished);
0210     });
0211 }
0212 
0213 void TaskHandler::itemsRemoved(const Item::List &items)
0214 {
0215     m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 task", "Removing %1 tasks", items.count()));
0216     qCDebug(GOOGLE_TASKS_LOG) << "Removing" << items.count() << "tasks";
0217     /* Google always automatically removes tasks with all their subtasks. In KOrganizer
0218      * by default we only remove the item we are given. For this reason we have to first
0219      * fetch all tasks, find all sub-tasks for the task being removed and detach them
0220      * from the task. Only then the task can be safely removed. */
0221     auto job = new ItemFetchJob(items.first().parentCollection());
0222     job->fetchScope().fetchFullPayload(true);
0223     connect(job, &ItemFetchJob::finished, this, [this, items](KJob *job) {
0224         if (job->error()) {
0225             m_iface->cancelTask(i18n("Failed to delete task: %1", job->errorString()));
0226             return;
0227         }
0228         const Item::List fetchedItems = qobject_cast<ItemFetchJob *>(job)->items();
0229         Item::List detachItems;
0230         TasksList detachTasks;
0231         for (const Item &fetchedItem : fetchedItems) {
0232             auto todo = fetchedItem.payload<KCalendarCore::Todo::Ptr>();
0233             TaskPtr task(new Task(*todo));
0234             const QString parentId = task->relatedTo(KCalendarCore::Incidence::RelTypeParent);
0235             if (parentId.isEmpty()) {
0236                 continue;
0237             }
0238 
0239             auto it = std::find_if(items.cbegin(), items.cend(), [&parentId](const Item &item) {
0240                 return item.remoteId() == parentId;
0241             });
0242             if (it != items.cend()) {
0243                 Item newItem(fetchedItem);
0244                 qCDebug(GOOGLE_TASKS_LOG) << "Detaching child" << newItem.remoteId() << "from" << parentId;
0245                 todo->setRelatedTo(QString(), KCalendarCore::Incidence::RelTypeParent);
0246                 newItem.setPayload<KCalendarCore::Todo::Ptr>(todo);
0247                 detachItems << newItem;
0248                 detachTasks << task;
0249             }
0250         }
0251         /* If there are no items do detach, then delete the task right now */
0252         if (detachItems.isEmpty()) {
0253             doRemoveTasks(items);
0254             return;
0255         }
0256 
0257         qCDebug(GOOGLE_TASKS_LOG) << "Reparenting" << detachItems.count() << "children...";
0258         auto moveJob = new TaskMoveJob(detachTasks, items.first().parentCollection().remoteId(), QString(), m_settings->accountPtr(), this);
0259         connect(moveJob, &TaskMoveJob::finished, this, [this, items, detachItems](KGAPI2::Job *job) {
0260             if (job->error()) {
0261                 m_iface->cancelTask(i18n("Failed to reparent subtasks: %1", job->errorString()));
0262                 return;
0263             }
0264             // Update items inside Akonadi DB too
0265             new ItemModifyJob(detachItems);
0266             // Perform actual removal
0267             doRemoveTasks(items);
0268         });
0269     });
0270 }
0271 
0272 void TaskHandler::doRemoveTasks(const Item::List &items)
0273 {
0274     // Make sure account is still valid
0275     if (!m_iface->canPerformTask()) {
0276         return;
0277     }
0278     QStringList taskIds;
0279     taskIds.reserve(items.count());
0280     std::transform(items.cbegin(), items.cend(), std::back_inserter(taskIds), [](const Item &item) {
0281         return item.remoteId();
0282     });
0283 
0284     /* Now finally we can safely remove the task we wanted to */
0285     auto job = new TaskDeleteJob(taskIds, items.first().parentCollection().remoteId(), m_settings->accountPtr(), this);
0286     job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items));
0287     connect(job, &TaskDeleteJob::finished, this, &TaskHandler::slotGenericJobFinished);
0288 }
0289 
0290 void TaskHandler::itemsMoved(const Item::List & /*item*/, const Collection & /*collectionSource*/, const Collection & /*collectionDestination*/)
0291 {
0292     m_iface->cancelTask(i18n("Moving tasks between task lists is not supported"));
0293 }
0294 
0295 void TaskHandler::collectionAdded(const Collection &collection, const Collection & /*parent*/)
0296 {
0297     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating new task list '%1'", collection.displayName()));
0298     qCDebug(GOOGLE_TASKS_LOG) << "Adding task list" << collection.displayName();
0299     TaskListPtr taskList(new TaskList());
0300     taskList->setTitle(collection.displayName());
0301 
0302     auto job = new TaskListCreateJob(taskList, m_settings->accountPtr(), this);
0303     connect(job, &TaskListCreateJob::finished, this, [this, collection](KGAPI2::Job *job) {
0304         if (!m_iface->handleError(job)) {
0305             return;
0306         }
0307 
0308         TaskListPtr taskList = qobject_cast<TaskListCreateJob *>(job)->items().first().dynamicCast<TaskList>();
0309         qCDebug(GOOGLE_TASKS_LOG) << "Task list created:" << taskList->uid();
0310         // Enable newly added task list in settings
0311         m_settings->addTaskList(taskList->uid());
0312         // Populate remoteId & other stuff
0313         Collection newCollection(collection);
0314         setupCollection(newCollection, taskList);
0315         m_iface->collectionChangeCommitted(newCollection);
0316         emitReadyStatus();
0317     });
0318 }
0319 
0320 void TaskHandler::collectionChanged(const Collection &collection)
0321 {
0322     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing task list '%1'", collection.displayName()));
0323     qCDebug(GOOGLE_TASKS_LOG) << "Changing task list" << collection.remoteId();
0324 
0325     TaskListPtr taskList(new TaskList());
0326     taskList->setUid(collection.remoteId());
0327     taskList->setTitle(collection.displayName());
0328     auto job = new TaskListModifyJob(taskList, m_settings->accountPtr(), this);
0329     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0330     connect(job, &TaskListModifyJob::finished, this, &TaskHandler::slotGenericJobFinished);
0331 }
0332 
0333 void TaskHandler::collectionRemoved(const Collection &collection)
0334 {
0335     m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing task list '%1'", collection.displayName()));
0336     qCDebug(GOOGLE_TASKS_LOG) << "Removing task list" << collection.remoteId();
0337     auto job = new TaskListDeleteJob(collection.remoteId(), m_settings->accountPtr(), this);
0338     job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
0339     connect(job, &TaskListDeleteJob::finished, this, &TaskHandler::slotGenericJobFinished);
0340 }