-
-
Save hiltmon/9786291 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby | |
# merge_asana_into_omnifocus.rb | |
# Hilton Lipschitz | |
# http://www.hiltmon.com | |
# Use and modify freely, attribution appreciated | |
# Script to import Asana projects and their tasks into | |
# OmniFocus and keep them up to date from Asana. | |
require "rubygems" | |
require "JSON" | |
require "net/https" | |
class MergeAsanaIntoOmnifocus | |
API_KEY = '***' | |
ASIGNEE_NAME = '***' | |
SUBTASKS = true | |
def get_json_data(url_string) | |
# set up HTTPS connection | |
uri = URI.parse(url_string) | |
http = Net::HTTP.new(uri.host, uri.port) | |
http.use_ssl = true | |
http.verify_mode = OpenSSL::SSL::VERIFY_PEER | |
# set up the request | |
header = { | |
"Content-Type" => "application/json" | |
} | |
req = Net::HTTP::Get.new(uri, header) | |
req.basic_auth(API_KEY, '') | |
# issue the request | |
res = http.start { |http| http.request(req) } | |
# Parse the result | |
body = JSON.parse(res.body) | |
if body['errors'] then | |
puts "Server returned an error: #{body['errors'][0]['message']}" | |
return nil | |
end | |
body | |
end | |
# ----------------------------------------------------------------------- | |
# ASANA API Calls | |
# ----------------------------------------------------------------------- | |
def get_projects | |
body = get_json_data("https://app.asana.com/api/1.0/projects") | |
projects = {} | |
body["data"].each do |element| | |
projects[element["id"]] = element["name"].gsub("'", '').gsub("\n", '') | |
end | |
projects | |
end | |
# NOTE: Assignee = me is ignored in projects filter | |
def get_tasks_in_project(project_id) | |
body = get_json_data("https://app.asana.com/api/1.0/tasks?project=#{project_id}&assignee=me") | |
tasks = {} | |
body["data"].each do |element| | |
tasks[element["id"]] = element["name"].gsub("'", '').gsub("\n", '') | |
end | |
tasks | |
end | |
def get_subtasks_in_task(task_id) | |
body = get_json_data("https://app.asana.com/api/1.0/tasks/#{task_id}/subtasks") | |
body["data"] | |
end | |
def get_project_detail(project_id) | |
body = get_json_data("https://app.asana.com/api/1.0/projects/#{project_id}") | |
body["data"] | |
end | |
def get_task_detail(task_id) | |
body = get_json_data("https://app.asana.com/api/1.0/tasks/#{task_id}") | |
body["data"] | |
end | |
# ----------------------------------------------------------------------- | |
# AppleScripts | |
# ----------------------------------------------------------------------- | |
def project_exists_osascript(project_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
try | |
set myProject to project "#{project_name}" in folder "Asana" | |
on error | |
-- Project is Missing, return failure code | |
return "Missing" | |
end try | |
return "Found" | |
end tell | |
end tell | |
} | |
end | |
# Note the hack to escape the single quote in Applescript's | |
def my_tasks_osascript(project_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set myProject to project "#{project_name}" in folder "Asana" | |
set myRowNames to name of every task of myProject | |
set AppleScript'"'"'s text item delimiters to "|" | |
set retVal to myRowNames as string | |
return retVal | |
end tell | |
end tell | |
} | |
end | |
def project_osascript(project_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
try | |
set myProject to project "#{project_name}" in folder "Asana" | |
on error | |
-- Project is Missing, create it | |
set myProject to make new project with properties {name: "#{project_name}"} at end of projects of myFolder | |
end try | |
end tell | |
end tell | |
} | |
end | |
def project_task_osascript(project_name, task_name, note, completed, due_date, context) | |
if due_date.nil? | |
due_date_str = '"none"' | |
else | |
due_date_str = "date \"#{due_date}\"" | |
end | |
completed_str = (completed ? 'true' : 'false') | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set myProject to project "#{project_name}" in folder "Asana" | |
set isCompleted to #{completed_str} | |
set dueDate to #{due_date_str} | |
set newContext to "#{context}" | |
if (newContext ≠ "none") then | |
set parentContext to context "People" | |
try | |
set myContext to first flattened context whose name is newContext | |
on error | |
-- Context is Missing, create it | |
tell parentContext | |
set myContext to make new context with properties {name:newContext} | |
end tell | |
end try | |
end if | |
try | |
set myTask to task "#{task_name}" in myProject | |
on error | |
-- Task is Missing, create it unless completed? | |
if (isCompleted = false) then | |
set myTask to make new task with properties {name:"#{task_name}"} at end of tasks of myProject | |
end if | |
end try | |
-- At this point, myTask exists if not completed | |
-- Since AppleScript has no test of undefined, wrap the next block | |
try | |
if (isCompleted = true) then | |
-- if it exists and is completed | |
tell myTask | |
set its completed to true | |
end tell | |
else | |
-- Update its notes and dates | |
tell myTask | |
set its note to "#{note}" | |
if (newContext ≠ "none") then | |
set its context to myContext | |
end if | |
if (dueDate ≠ "none") then | |
set its due date to dueDate | |
end if | |
end tell | |
end if | |
on error | |
-- No, just the task is complete, no clutter in OmniFocus | |
end try | |
end tell | |
end tell | |
} | |
end | |
def my_task_complete_osascript(project_name, task_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set myProject to project "#{project_name}" in folder "Asana" | |
set isCompleted to true | |
try | |
set myTask to task "#{task_name}" in myProject | |
tell myTask | |
set its completed to true | |
end tell | |
on error | |
-- Task is Missing, ignore it | |
end try | |
end tell | |
end tell | |
} | |
end | |
# Note the hack to escape the single quote in Applescript's | |
def my_subtasks_osascript(project_name, parent_task_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana" | |
set myRowNames to name of every task of parentTask | |
set AppleScript'"'"'s text item delimiters to "|" | |
set retVal to myRowNames as string | |
return retVal | |
end tell | |
end tell | |
} | |
end | |
def project_sub_task_osascript(project_name, parent_task_name, task_name, note, completed, due_date, context) | |
if due_date.nil? | |
due_date_str = '"none"' | |
else | |
due_date_str = "date \"#{due_date}\"" | |
end | |
completed_str = (completed ? 'true' : 'false') | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana" | |
set isCompleted to #{completed_str} | |
set dueDate to #{due_date_str} | |
set newContext to "#{context}" | |
if (newContext ≠ "none") then | |
set parentContext to context "People" | |
try | |
set myContext to first flattened context whose name is newContext | |
on error | |
-- Context is Missing, create it | |
tell parentContext | |
set myContext to make new context with properties {name:newContext} | |
end tell | |
end try | |
end if | |
try | |
set myTask to task "#{task_name}" in parentTask | |
on error | |
-- Task is Missing, create it unless completed? | |
if (isCompleted = false) then | |
set myTask to make new task with properties {name:"#{task_name}"} at end of tasks of parentTask | |
end if | |
end try | |
-- At this point, myTask exists if not completed | |
-- Since AppleScript has no test of undefined, wrap the next block | |
try | |
if (isCompleted = true) then | |
-- if it exists and is completed | |
tell myTask | |
set its completed to true | |
end tell | |
else | |
-- Update its notes and dates | |
tell myTask | |
set its note to "#{note}" | |
if (newContext ≠ "none") then | |
set its context to myContext | |
end if | |
if (dueDate ≠ "none") then | |
set its due date to dueDate | |
end if | |
end tell | |
end if | |
on error | |
-- No, just the task is complete, no clutter in OmniFocus | |
end try | |
end tell | |
end tell | |
} | |
end | |
def my_sub_task_complete_osascript(project_name, parent_task_name, sub_task_name) | |
%Q{ | |
tell application "OmniFocus" | |
tell front document | |
set myFolder to folder "Asana" | |
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana" | |
set isCompleted to true | |
try | |
set myTask to task "#{sub_task_name}" in parentTask | |
tell myTask | |
set its completed to true | |
end tell | |
on error | |
-- Task is Missing, ignore it | |
end try | |
end tell | |
end tell | |
} | |
end | |
# ----------------------------------------------------------------------- | |
# MAIN LOOP | |
# ----------------------------------------------------------------------- | |
def run | |
projects = get_projects | |
projects.each_pair do |project_id, project_name| | |
detail = get_project_detail(project_id) | |
# Skip archived projects if not in Omnifocus | |
if detail['archived'] == true | |
result = %x{osascript -e '#{project_exists_osascript(project_name)}'} | |
# puts "#{project_name}: [#{result}]" | |
if result == 'Missing' | |
puts "Skipping Archived Project: #{project_name}..." | |
next | |
end | |
end | |
# Create or update the project in OmniFocus using AppleScript | |
%x{osascript -e '#{project_osascript(project_name)}'} | |
# Project / Tasks | |
puts "Tasks for #{project_name}..." | |
tasks = get_tasks_in_project(project_id) | |
# Get my known tasks | |
result = %x{osascript -e '#{my_tasks_osascript(project_name)}'} | |
my_known_tasks = result.strip.split('|') | |
tasks.each_pair do |task_id, task_name| | |
next if task_name == "" # We seem to have these! | |
detail = get_task_detail(task_id) | |
# puts detail if project_name == 'Single cusip view' | |
# Create or update a task in a project | |
notes = detail['notes'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '') | |
task_name = task_name.gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '') | |
context = 'none' | |
unless detail['assignee'].nil? | |
if detail['assignee']['name'] != ASIGNEE_NAME | |
context = detail['assignee']['name'] | |
end | |
end | |
%x{osascript -e '#{project_task_osascript(project_name, task_name, notes, detail['completed'], detail['due_on'], context)}'} | |
# Remove from my known as we see it in Asana | |
# puts "///DEL/// [#{task_name}]" | |
my_known_tasks.delete(task_name) | |
next if detail['completed'] == true # Ignore subtasks | |
if SUBTASKS == true | |
# Project / Task / SubTask | |
puts " Subtasks for #{task_name}..." | |
subtasks = get_subtasks_in_task(task_id) | |
# Get my known sub-tasks | |
result = %x{osascript -e '#{my_subtasks_osascript(project_name, task_name)}'} | |
my_known_sub_tasks = result.strip.split('|') | |
if subtasks.length > 0 | |
subtasks.each do |sub_task| | |
next if sub_task["name"] == "" | |
sub_detail = get_task_detail(sub_task["id"]) | |
sub_context = context | |
unless detail['assignee'].nil? | |
if detail['assignee']['name'] != ASIGNEE_NAME | |
sub_context = detail['assignee']['name'] | |
end | |
end | |
# Create or update a subtask in Omnifocus | |
sub_notes = sub_detail['notes'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '') | |
sub_task_name = sub_task['name'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '') | |
%x{osascript -e '#{project_sub_task_osascript(project_name, task_name, sub_task_name, sub_notes, sub_detail['completed'], sub_detail['due_on'], sub_context)}'} | |
# Remove from my known as we see it in Asana | |
# puts "///DEL/// #{task_name}" | |
my_known_sub_tasks.delete(sub_task_name) | |
end | |
end | |
my_known_sub_tasks.each do |sub_task_name| | |
sub_task_name.strip! | |
next if sub_task_name == "" # We seem to have these! | |
puts "Missing: #{sub_task_name} under #{task_name} // Marked as completed" | |
%x{osascript -e '#{my_sub_task_complete_osascript(project_name, task_name, sub_task_name)}'} | |
end | |
end | |
end | |
my_known_tasks.each do |task_name| | |
task_name.strip! | |
next if task_name == "" # We seem to have these! | |
puts "Missing: #{task_name} // Marked as completed" | |
%x{osascript -e '#{my_task_complete_osascript(project_name, task_name)}'} | |
end | |
end | |
end | |
def test | |
project_name = 'Single cusip view' | |
result = %x{osascript -e '#{my_tasks_osascript(project_name)}'} | |
puts result.split('|') | |
end | |
end | |
app = MergeAsanaIntoOmnifocus.new() | |
app.run() | |
# app.test() |
hey this works great on OmniFocus 2.0.4 (v87.19), pro edition. it should be noted somewhere, perhaps in comments at the top of this script that you need a folder called "Asana" in your projects area for this to work.
Awsome, it does work fine with OmniFocus Pro 2.1 too! Many thanks! (using huyz script). But I am too stupid to understand the time format (no idea of coding) - the script does give me errors on the time & date format - using a non US time format on the computer. If I set the time format to US, works like a charme.
Could someone explain to me what I need to change exactly in the script to make it work with Eu or Australian settings? Unfortunately there is no way to change Asana to those settings. many thanks again!
FYI -- I had to make some minor changes (really from the Asana side) but it still works in OmniFocus 3
my fork is here:
https://gist.github.com/squarepegsys/8e9dd706773c2b2e480b9e78b93b52e1
Thanks for that.
A couple of fixes in my fork: https://gist.github.com/huyz/9caa5e95b7a1f6f8f356