-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
05bd930
commit 8d54cec
Showing
5 changed files
with
307 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |