Google Tasks to Org
I previously posted on how to use Google Assistant to voice capture notes, which can subsequently be downloaded into an org file. This approach no longer works, since the service used there was deprecated.
While I could not replicate that entire pipeline, specifically the voice part, I at least found a way to capture tasks on Android and transfer them to org mode.
I chose Google Tasks since (a) it has an API (unlike Keep) and (b) there is a strong possibility that we’ll have voice capture for tasks in the near future 🤞.
Implementation
First, install Google Tasks on your phone, add a new TODO list, and add a few tasks.
Next, we are going to set up an Apps Script service that will convert our tasks to org format, and expose the result via a URL.
- Create a new Google Sheets spreadsheet, named whatever you like (say,
Google Tasks to Org
). - Click on
Extensions ➡ Apps Script
; the editor opens. Name the app (say,tasks-to-org
). - In the sidebar, click
Services
and enable the Tasks API. - Paste the following into the editor:
// Once this code is deployed as a web app, you can call it via curl:
//
// URL="..."
// TOKEN="..."
// curl -s -S -L -d "$TOKEN" "$URL?clear=1" >> output.org
//
// Remember to customize `token` and `taskList` below.
function doPost(e) {
// A token (your password) can be anything; you can generate it using, e.g., Python:
//
// python -c "import base64, os; print(base64.b64encode(os.urandom(50)).decode('ascii'))"
//
const token = "abcdefg12345";
// The name of the task list in Google Tasks
// This name is case sensitive
const taskList = "org";
var response = "Invalid token";
const clear = (e.parameter['clear'] === '1');
if (e.postData.contents === token) {
response = tasksToOrg(taskList, clear);
}
return ContentService.createTextOutput(response);
}
function doGet(e) {
return ContentService.createTextOutput("OK");
}
function taskToOrg(task) {
// See https://developers.google.com/tasks/reference/rest/v1/tasks#Task
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
var entry = '* ';
const status = (task.status == "completed" ? "DONE" : "TODO");
var deadline = '';
var notes = '';
if (task.due) {
const due = new Date(task.due);
const year = due.getFullYear().toString();
const month = (due.getMonth() + 1).toString().padStart(2, '0');
const day = due.getDate().toString().padStart(2, '0');
const wkday = days[due.getDay()];
deadline = `\n DEADLINE: <${year}-${month}-${day} ${wkday}>`;
}
if (task.notes) {
notes = task.notes.split("\n");
for (let i = 0; i < notes.length; i++) {
notes[i] = "\n " + notes[i];
}
notes = notes.join("");
}
return `* ${status} ${task.title}${deadline}${notes}`;
}
function tasksToOrg(taskListTitle, clear) {
var org_file = [];
const taskLists = Tasks.Tasklists.list({maxResults: 100, showDeleted: true, showCompleted: true, showHidden: true}).items;
const taskList = taskLists.filter(tl => tl.title === taskListTitle)[0];
const taskListId = taskList.id;
const tasks = Tasks.Tasks.list(taskListId);
for (let i = 0; i < tasks.items.length; i++) {
const task = tasks.items[i];
org_file.push(taskToOrg(task));
}
if (clear) {
for (let i = 0; i < tasks.items.length; i++) {
const task = tasks.items[i];
Tasks.Tasks.remove(taskListId, task.id);
}
}
return org_file.join("\n");
}
Deployment
We want to expose this script at a public URL, so go to Deploy ➡ New Deployment
(Deploy
is in the upper-righthand corner).
Set Execute as
to your email and Who has access
to anyone.
It needs to run as yourself, to access your tasks, and we need anyone to have access, since you will be calling the URL anonymously from the terminal (not signed in from a browser).
Click deploy.
Google will ask you to authenticate the app, and warn you that the app is not officially authenticated. Click away the warnings and proceed (you trust yourself, don’t you?).
Now, you should be presented with a URL which, when accessed, runs the app.
Configuration
Set the variable token
to any long, hard-to-guess string of your choice. You can generate one with Python:
python -c "import base64, os; print(base64.b64encode(os.urandom(50)).decode('ascii'))"
Set the variable taskList
to the same name as the list in Google Tasks.
Task list to org
We can now access our app via a POST
request. E.g., using curl
:
curl -s -S -L -d "abcdefg12345 your token value" https://script.google.com/macros/s/abc123-app-id-generated-by-google/exec
This should yield something like:
* TODO First task
* TODO A task for today
DEADLINE: <2023-01-11 Wed>
* TODO Another task
This time, the task has a description.
To clear the tasks after downloading, add a clear=1
argument:
curl -s -S -L -d "abcdefg12345 your token value" https://script.google.com/macros/s/abc123-app-id-generated-by-google/exec?clear=1
Here’s the general script I use:
#!/bin/bash
TASKS_TO_ORG_URL="https://script.google.com/macros/s/.../exec"
ORG_INBOX="${HOME}/org/tasks-inbox.org"
TOKEN="abcdefg12345"
curl -s -S -L -d "$TOKEN" "$TASKS_TO_ORG_URL?clear=1" >> $ORG_INBOX
In my daily org planner, I then put:
- [ ] ([[shell:~/scripts/google-tasks-to-org.sh][fetch]]) [[file:~/org/tasks-inbox.org][Google Tasks]]