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 }