File indexing completed on 2024-05-12 05:22:33

0001 /*
0002     SPDX-FileCopyrightText: 2012-2018 Daniel Vrátil <dvratil@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "tasksservice.h"
0008 #include "object.h"
0009 #include "task.h"
0010 #include "tasklist.h"
0011 #include "utils.h"
0012 
0013 #include <QJsonDocument>
0014 #include <QUrlQuery>
0015 #include <QVariant>
0016 
0017 namespace KGAPI2
0018 {
0019 
0020 namespace TasksService
0021 {
0022 
0023 namespace
0024 {
0025 /* Google accepts only UTC time strictly in this format :( */
0026 static const auto DatetimeFormat = QStringLiteral("yyyy-MM-ddThh:mm:ss.zzzZ");
0027 
0028 static const auto TasksUrlPart = QLatin1StringView("/tasks");
0029 
0030 static const auto PageTokenParam = QStringLiteral("pageToken");
0031 static const auto MaxResultsParam = QStringLiteral("maxResults");
0032 static const auto MaxResultsParamValueDefault = QStringLiteral("20");
0033 
0034 static const auto KindAttr = QStringLiteral("kind");
0035 static const auto IdAttr = QStringLiteral("id");
0036 static const auto EtagAttr = QStringLiteral("etag");
0037 static const auto TitleAttr = QStringLiteral("title");
0038 static const auto NotesAttr = QStringLiteral("notes");
0039 static const auto ItemsAttr = QStringLiteral("items");
0040 static const auto NextPageTokenAttr = QStringLiteral("nextPageToken");
0041 static const auto ParentAttr = QStringLiteral("parent");
0042 static const auto SelfLinkAttr = QStringLiteral("selfLink");
0043 static const auto UpdatedAttr = QStringLiteral("updated");
0044 static const auto StatusAttr = QStringLiteral("status");
0045 static const auto DueAttr = QStringLiteral("due");
0046 static const auto DeletedAttr = QStringLiteral("deleted");
0047 
0048 static const auto CompletedAttrVal = QLatin1StringView("completed");
0049 static const auto NeedsActionAttrVal = QLatin1StringView("needsAction");
0050 }
0051 
0052 namespace Private
0053 {
0054 ObjectsList parseTaskListJSONFeed(const QVariantList &items);
0055 ObjectsList parseTasksJSONFeed(const QVariantList &items);
0056 
0057 ObjectPtr JSONToTaskList(const QVariantMap &jsonData);
0058 ObjectPtr JSONToTask(const QVariantMap &jsonData);
0059 
0060 static const QUrl GoogleApisUrl(QStringLiteral("https://www.googleapis.com"));
0061 static const QString TasksBasePath(QStringLiteral("/tasks/v1/lists"));
0062 static const QString TasksListsBasePath(QStringLiteral("/tasks/v1/users/@me/lists"));
0063 }
0064 
0065 ObjectsList parseJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
0066 {
0067     QJsonDocument document = QJsonDocument::fromJson(jsonFeed);
0068     if (document.isNull()) {
0069         return ObjectsList();
0070     }
0071 
0072     ObjectsList list;
0073     const QVariantMap feed = document.toVariant().toMap();
0074 
0075     if (feed.value(KindAttr).toString() == QLatin1StringView("tasks#taskLists")) {
0076         list = Private::parseTaskListJSONFeed(feed.value(ItemsAttr).toList());
0077 
0078         if (feed.contains(NextPageTokenAttr)) {
0079             feedData.nextPageUrl = fetchTaskListsUrl();
0080             QUrlQuery query(feedData.nextPageUrl);
0081             query.addQueryItem(PageTokenParam, feed.value(NextPageTokenAttr).toString());
0082             if (query.queryItemValue(MaxResultsParam).isEmpty()) {
0083                 query.addQueryItem(MaxResultsParam, MaxResultsParamValueDefault);
0084             }
0085             feedData.nextPageUrl.setQuery(query);
0086         }
0087 
0088     } else if (feed.value(KindAttr).toString() == QLatin1StringView("tasks#tasks")) {
0089         list = Private::parseTasksJSONFeed(feed.value(ItemsAttr).toList());
0090 
0091         if (feed.contains(NextPageTokenAttr)) {
0092             QString taskListId = feedData.requestUrl.toString().remove(QStringLiteral("https://www.googleapis.com/tasks/v1/lists/"));
0093             taskListId = taskListId.left(taskListId.indexOf(QLatin1Char('/')));
0094 
0095             feedData.nextPageUrl = fetchAllTasksUrl(taskListId);
0096             QUrlQuery query(feedData.nextPageUrl);
0097             query.addQueryItem(PageTokenParam, feed.value(NextPageTokenAttr).toString());
0098             if (query.queryItemValue(MaxResultsParam).isEmpty()) {
0099                 query.addQueryItem(MaxResultsParam, MaxResultsParamValueDefault);
0100             }
0101             feedData.nextPageUrl.setQuery(query);
0102         }
0103     }
0104 
0105     return list;
0106 }
0107 
0108 QUrl fetchAllTasksUrl(const QString &tasklistID)
0109 {
0110     QUrl url(Private::GoogleApisUrl);
0111     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart);
0112     return url;
0113 }
0114 
0115 QUrl fetchTaskUrl(const QString &tasklistID, const QString &taskID)
0116 {
0117     QUrl url(Private::GoogleApisUrl);
0118     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart % QLatin1Char('/') % taskID);
0119     return url;
0120 }
0121 
0122 QUrl createTaskUrl(const QString &tasklistID)
0123 {
0124     QUrl url(Private::GoogleApisUrl);
0125     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart);
0126     return url;
0127 }
0128 
0129 QUrl updateTaskUrl(const QString &tasklistID, const QString &taskID)
0130 {
0131     QUrl url(Private::GoogleApisUrl);
0132     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart % QLatin1Char('/') % taskID);
0133     return url;
0134 }
0135 
0136 QUrl removeTaskUrl(const QString &tasklistID, const QString &taskID)
0137 {
0138     QUrl url(Private::GoogleApisUrl);
0139     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart % QLatin1Char('/') % taskID);
0140     return url;
0141 }
0142 
0143 QUrl moveTaskUrl(const QString &tasklistID, const QString &taskID, const QString &newParent)
0144 {
0145     QUrl url(Private::GoogleApisUrl);
0146     url.setPath(Private::TasksBasePath % QLatin1Char('/') % tasklistID % TasksUrlPart % QLatin1Char('/') % taskID % QLatin1StringView("/move"));
0147     if (!newParent.isEmpty()) {
0148         QUrlQuery query(url);
0149         query.addQueryItem(ParentAttr, newParent);
0150         url.setQuery(query);
0151     }
0152 
0153     return url;
0154 }
0155 
0156 QUrl fetchTaskListsUrl()
0157 {
0158     QUrl url(Private::GoogleApisUrl);
0159     url.setPath(Private::TasksListsBasePath);
0160     return url;
0161 }
0162 
0163 QUrl createTaskListUrl()
0164 {
0165     QUrl url(Private::GoogleApisUrl);
0166     url.setPath(Private::TasksListsBasePath);
0167     return url;
0168 }
0169 
0170 QUrl updateTaskListUrl(const QString &tasklistID)
0171 {
0172     QUrl url(Private::GoogleApisUrl);
0173     url.setPath(Private::TasksListsBasePath % QLatin1Char('/') % tasklistID);
0174     return url;
0175 }
0176 
0177 QUrl removeTaskListUrl(const QString &tasklistID)
0178 {
0179     QUrl url(Private::GoogleApisUrl);
0180     url.setPath(Private::TasksListsBasePath % QLatin1Char('/') % tasklistID);
0181     return url;
0182 }
0183 
0184 /******************************* PRIVATE ******************************/
0185 
0186 ObjectPtr Private::JSONToTaskList(const QVariantMap &jsonData)
0187 {
0188     TaskListPtr taskList(new TaskList());
0189 
0190     taskList->setUid(jsonData.value(IdAttr).toString());
0191     taskList->setEtag(jsonData.value(EtagAttr).toString());
0192     taskList->setTitle(jsonData.value(TitleAttr).toString());
0193     taskList->setSelfLink(jsonData.value(SelfLinkAttr).toString());
0194     taskList->setUpdated(jsonData.value(UpdatedAttr).toString());
0195 
0196     return taskList.dynamicCast<Object>();
0197 }
0198 
0199 TaskListPtr JSONToTaskList(const QByteArray &jsonData)
0200 {
0201     QJsonDocument document = QJsonDocument::fromJson(jsonData);
0202     const QVariantMap data = document.toVariant().toMap();
0203 
0204     if (data.value(KindAttr).toString() == QLatin1StringView("tasks#taskList")) {
0205         return Private::JSONToTaskList(data).staticCast<TaskList>();
0206     }
0207 
0208     return TaskListPtr();
0209 }
0210 
0211 ObjectPtr Private::JSONToTask(const QVariantMap &jsonData)
0212 {
0213     TaskPtr task(new Task());
0214 
0215     task->setUid(jsonData.value(IdAttr).toString());
0216     task->setEtag(jsonData.value(EtagAttr).toString());
0217     task->setSummary(jsonData.value(TitleAttr).toString());
0218     task->setLastModified(Utils::rfc3339DateFromString(jsonData.value(UpdatedAttr).toString()));
0219     task->setDescription(jsonData.value(NotesAttr).toString());
0220 
0221     if (jsonData.value(StatusAttr).toString() == NeedsActionAttrVal) {
0222         task->setStatus(KCalendarCore::Incidence::StatusNeedsAction);
0223     } else if (jsonData.value(StatusAttr).toString() == CompletedAttrVal) {
0224         task->setStatus(KCalendarCore::Incidence::StatusCompleted);
0225     } else {
0226         task->setStatus(KCalendarCore::Incidence::StatusNone);
0227     }
0228 
0229     // "due" is date-only -- no time-of-day given.
0230     task->setAllDay(true);
0231     task->setDtDue(Utils::rfc3339DateFromString(jsonData.value(DueAttr).toString()));
0232 
0233     if (task->status() == KCalendarCore::Incidence::StatusCompleted) {
0234         task->setCompleted(Utils::rfc3339DateFromString(jsonData.value(CompletedAttrVal).toString()));
0235     }
0236 
0237     task->setDeleted(jsonData.value(DeletedAttr).toBool());
0238 
0239     if (jsonData.contains(ParentAttr)) {
0240         task->setRelatedTo(jsonData.value(ParentAttr).toString(), KCalendarCore::Incidence::RelTypeParent);
0241     }
0242 
0243     return task.dynamicCast<Object>();
0244 }
0245 
0246 TaskPtr JSONToTask(const QByteArray &jsonData)
0247 {
0248     QJsonDocument document = QJsonDocument::fromJson(jsonData);
0249     const QVariantMap data = document.toVariant().toMap();
0250 
0251     if (data.value(KindAttr).toString() == QLatin1StringView("tasks#task")) {
0252         return Private::JSONToTask(data).staticCast<Task>();
0253     }
0254 
0255     return TaskPtr();
0256 }
0257 
0258 QByteArray taskListToJSON(const TaskListPtr &taskList)
0259 {
0260     QVariantMap output;
0261 
0262     output.insert(KindAttr, QStringLiteral("tasks#taskList"));
0263     if (!taskList->uid().isEmpty()) {
0264         output.insert(IdAttr, taskList->uid());
0265     }
0266     output.insert(TitleAttr, taskList->title());
0267 
0268     QJsonDocument document = QJsonDocument::fromVariant(output);
0269     return document.toJson(QJsonDocument::Compact);
0270 }
0271 
0272 QByteArray taskToJSON(const TaskPtr &task)
0273 {
0274     QVariantMap output;
0275 
0276     output.insert(KindAttr, QStringLiteral("tasks#task"));
0277 
0278     if (!task->uid().isEmpty()) {
0279         output.insert(IdAttr, task->uid());
0280     }
0281 
0282     output.insert(TitleAttr, task->summary());
0283     output.insert(NotesAttr, task->description());
0284 
0285     if (!task->relatedTo(KCalendarCore::Incidence::RelTypeParent).isEmpty()) {
0286         output.insert(ParentAttr, task->relatedTo(KCalendarCore::Incidence::RelTypeParent));
0287     }
0288 
0289     if (task->dtDue().isValid()) {
0290         output.insert(DueAttr, task->dtDue().toUTC().toString(DatetimeFormat));
0291     }
0292 
0293     if ((task->status() == KCalendarCore::Incidence::StatusCompleted) && task->completed().isValid()) {
0294         output.insert(CompletedAttrVal, task->completed().toUTC().toString(DatetimeFormat));
0295         output.insert(StatusAttr, CompletedAttrVal);
0296     } else {
0297         output.insert(StatusAttr, NeedsActionAttrVal);
0298     }
0299 
0300     QJsonDocument document = QJsonDocument::fromVariant(output);
0301     return document.toJson(QJsonDocument::Compact);
0302 }
0303 
0304 ObjectsList Private::parseTaskListJSONFeed(const QVariantList &items)
0305 {
0306     ObjectsList list;
0307     list.reserve(items.size());
0308     for (const QVariant &item : items) {
0309         list.append(Private::JSONToTaskList(item.toMap()));
0310     }
0311 
0312     return list;
0313 }
0314 
0315 ObjectsList Private::parseTasksJSONFeed(const QVariantList &items)
0316 {
0317     ObjectsList list;
0318     list.reserve(items.size());
0319     for (const QVariant &item : items) {
0320         list.append(Private::JSONToTask(item.toMap()));
0321     }
0322 
0323     return list;
0324 }
0325 
0326 } // namespace TasksService
0327 
0328 } // namespace KGAPI2