Skip to content

Commit

Permalink
Implementation of Basic features
Browse files Browse the repository at this point in the history
  • Loading branch information
sn00py1310 authored Jun 2, 2023
1 parent 05bd930 commit 8d54cec
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 0 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
# Habitica Google Tasks Sync

## Features
- Rate Limit efficient, only changes updated Google Tasks
- One-way synchronization from Google Tasks to Habitica
- Syncs tasks that have been unmarked again (with a limit of Habitica auto delete and only 30 latest marked tasks)
- Auto deletes Habitica tasks when Google Task got deleted
- Add default Tags for easy searching

## Limitations
- No synchronization when a Task is moved to excluded Task Lists
- Subtasks a treated as their individual tasks
- No due date implementation
- If too many changes between the executions the script can crash.

## Setup
### Installation
1. Go to the [latest release](./releases/latest) and download the .zip or .tar.gz file.
2. Create a Google App Script (GAS) Project [here](https://script.google.com).
3. Copy the content of the `src` folder in the downloaded release to the create GAS Project.
4. Get your Habitica API user and token from [here](https://habitica.com/user/settings/api).
5. Setup the project variables for the GAS Project under the settings, see [Setting](#settings).
6. In the GAS Project create a trigger for the function main. Set the repeating time to 5min.
7. Go to the main file and run the main method once, now you should get a pop-up to request access to your Task and to send external requests.
8. See the Tasks sync to Habitica.


### Settings
| Optional | Key | Value |
| --- | --- | --- |
| :x: | `habitica_api_key` | The Api key for your Habitica account |
| :x: | `habitica_api_user` | The Api user for your Habitica account |
| :heavy_check_mark: | `habitica_tags` | A comma (`,`) separated list of tags to add to the Tasks |
| :heavy_check_mark: | `excludedTaskLists` | A comma (`,`) separated list of Google Tasks List id to exclude |
| :heavy_check_mark: | `lastRun` | `System Settig` A timestamp to keep track of the last runs, to only get update Google Tasks |
31 changes: 31 additions & 0 deletions src/GTask.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function getAllTasksWithListData() {
const taskLists = Tasks.Tasklists.list(taskListReqSettigs);
if (!taskLists.items) {
console.log("No task lists found.");
return [];
}

let allTasks = [];
for (const taskList of taskLists.items) {
if (getExcludedTaskLists().includes(taskList.id)) continue;

console.info(
`Task list with title "${taskList.title}" and ID "${taskList.id}" was found.`
);
let tasks = getAllGTasks(taskList.id);

if (tasks.items.length !== 0) {
allTasks.push({
id: taskList.id,
title: taskList.title,
tasks: tasks.items,
});
}
}

return allTasks;
}

function getAllGTasks(taskListId) {
return Tasks.Tasks.list(taskListId, taskReqSettigs);
}
119 changes: 119 additions & 0 deletions src/Habitica.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
function fechtTasksApi(todoType) {
const response = UrlFetchApp.fetch(
`${baseApi}/tasks/user?type=${todoType}`,
getDefaultParams()
);

let resData = JSON.parse(response.getContentText());
if (!resData.success) {
throw new Error(
`Request Error, ${response.getResponseCode()} - ${response.getContentText()}`
);
}
return skipOtherTasks(resData.data);
}

function getAllTasksApi() {
let allTasks = [];
for (const type of ["todos", "completedTodos"]) {
allTasks = allTasks.concat(fechtTasksApi(type));
}
return allTasks;
}

function skipOtherTasks(data) {
let tasks = [];
for (const t of data) {
if (!t.alias || !t.alias.startsWith(aliasPrefix)) continue;
tasks.push(t);
}
return tasks;
}

function createTaskApi(payload) {
let params = getDefaultParams();
params.method = "post";
params.payload = JSON.stringify(payload);

const response = UrlFetchApp.fetch(`${baseApi}/tasks/user`, params);
let resData = JSON.parse(response.getContentText());
if (!resData.success) {
throw new Error(
`Request Error, ${response.getResponseCode()} - ${response.getContentText()}`
);
}
}

function updateTaskApi(payload, taskId) {
let params = getDefaultParams();
params.method = "put";
params.payload = JSON.stringify(payload);

const response = UrlFetchApp.fetch(`${baseApi}/tasks/${taskId}`, params);
let resData = JSON.parse(response.getContentText());
if (!resData.success) {
throw new Error(
`Request Error, ${response.getResponseCode()} - ${response.getContentText()}`
);
}
}

function deleteTaskApi(taskId) {
let params = getDefaultParams();
params.method = "delete";

const response = UrlFetchApp.fetch(`${baseApi}/tasks/${taskId}`, params);
let resData = JSON.parse(response.getContentText());
if (!resData.success) {
throw new Error(
`Request Error, ${response.getResponseCode()} - ${response.getContentText()}`
);
}
}

function scoreTaskApi(taskId, direction) {
let params = getDefaultParams();
params.method = "post";

const response = UrlFetchApp.fetch(
`${baseApi}/tasks/${taskId}/score/${direction}`,
params
);

let resData = JSON.parse(response.getContentText());
if (!resData.success) {
throw new Error(
`Request Error, ${response.getResponseCode()} - ${response.getContentText()}`
);
}
}

class CustomTask {
constructor(gTask, listTitle, listId) {
this.gTask = gTask;
this.listTitle = listTitle;
this.listId = listId;
}

getId() {
return aliasPrefix + this.gTask.id;
}

patchPayload() {
const g = this.gTask;
let p = {};
p.type = "todo";
p.text = `[${this.listTitle}] ${g.title} (GTasks)`;
if (habiticaTags) p.tags = habiticaTags;

p.notes = `${g.notes ?? ""} (Last Updated: ${new Date().toLocaleString()})`;

return p;
}

createPayload() {
let p = this.patchPayload();
p.alias = this.getId();
return p;
}
}
64 changes: 64 additions & 0 deletions src/Main.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function main() {
const lastRun = getLastRunTime();
Logger.log(lastRun);

try {
tasksHandler();
updateLastRunTime();
} catch (e) {
console.warn(e.message);
}
}

function tasksHandler() {
let gTasksListsFiltered = getAllTasksWithListData();
if (!gTasksListsFiltered) return;

let hTaskDict = {};
let hTasks = getAllTasksApi();
for (const hTask of hTasks) {
hTaskDict[hTask.alias] = hTask;
}

for (const gTasksWithListData of gTasksListsFiltered) {
const listId = gTasksWithListData.id;
const listTitle = gTasksWithListData.title;
const gTasks = gTasksWithListData.tasks;

for (const gTask of gTasks) {
let alias = aliasPrefix + gTask.id;

let customTask = new CustomTask(gTask, listTitle, listId);
if (hTaskDict[alias]) updateHTasks(customTask, hTaskDict[alias]);
else createHTask(customTask);
}
console.info(`Updated ${gTasks.length} tasks in ${listTitle} [${listId}]`);
}
}

function updateHTasks(customTask, hTask) {
if (customTask.gTask.deleted) {
deleteTaskApi(customTask.getId());
return;
}

updateTaskApi(customTask.patchPayload(), customTask.getId());
taskScorer(customTask, hTask.completed);
}

function createHTask(customTask) {
if (customTask.gTask.deleted) return;

createTaskApi(customTask.createPayload());
taskScorer(customTask, false);
}

function taskScorer(customTask, hTaskCompleted) {
const gTask = customTask.gTask;
let gTaskCompleted = gTask.status == "completed";

if (gTaskCompleted != hTaskCompleted) {
let direction = gTaskCompleted ? "up" : "down";
scoreTaskApi(customTask.getId(), direction);
}
}
59 changes: 59 additions & 0 deletions src/Settings.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const properties = PropertiesService.getScriptProperties();
const aliasPrefix = "GTasks_";

function getLastRunTime() {
return new Date(properties.getProperty("lastRun"));
}

function updateLastRunTime() {
properties.setProperty("lastRun", new Date());
}

const excludedTaskLists = properties.getProperty("excludedTaskLists")
? properties.getProperty("excludedTaskLists").split(",")
: [];
function getExcludedTaskLists() {
return excludedTaskLists;
}

const habiticaTags = properties.getProperty("habitica_tags")
? properties.getProperty("habitica_tags").split(",")
: [];

function clearAllProperties() {
PropertiesService.getUserProperties().deleteAllProperties();
PropertiesService.getScriptProperties().deleteAllProperties();
}

const taskListReqSettigs = {
showHidden: true,
maxResults: 100,
};

const taskReqSettigs = {
showHidden: true,
showDeleted: true,
maxResults: 100,
updatedMin: getLastRunTime().toISOString(),
};

const baseApi = "https://habitica.com/api/v4";

const baseHeaders = {
"x-api-key": properties.getProperty("habitica_api_key"),
"x-api-user": properties.getProperty("habitica_api_user"),
"content-type": "application/json",
};

const defaultParams = {
headers: baseHeaders,
muteHttpExceptions: true,
};

function getDefaultParams() {
return copyObj(defaultParams);
}

function copyObj(obj) {
return JSON.parse(JSON.stringify(obj));
}

0 comments on commit 8d54cec

Please sign in to comment.