Skip to content

Instantly share code, notes, and snippets.

@hiltmon
Last active October 2, 2018 17:58
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save hiltmon/d1f79e95dd11252ce6ca to your computer and use it in GitHub Desktop.
Save hiltmon/d1f79e95dd11252ce6ca to your computer and use it in GitHub Desktop.
Updated Merge Asana into OmniFocus script
#!/usr/bin/env ruby -E utf-8
# 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.
# 2015-01-24: Added "No-Project" tasks in all workspaces
require "rubygems"
require "JSON"
require "date"
require "net/https"
class MergeAsanaIntoOmnifocus
API_KEY = '<YOUR KEY HERE>'
ASIGNEE_NAME = '<YOUR PROFILE NAME HERE>'
SUBTASKS = true
DATE_FORMAT = '%A %B %d, %Y at %H:%M:%S' # (Try http://strftimer.com to build your own)
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_workspaces
body = get_json_data("https://app.asana.com/api/1.0/workspaces")
workspaces = {}
body["data"].each do |element|
workspaces[element["id"]] = element["name"].gsub("'", '').gsub("\n", '')
end
workspaces
end
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
def get_tasks_in_no_project(workspace_id)
body = get_json_data("https://app.asana.com/api/1.0/tasks?workspace=#{workspace_id}&assignee=me")
tasks = {}
body["data"].each do |element|
tasks[element["id"]] = element["name"].gsub("'", '').gsub("\n", '')
end
tasks
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 = Date.parse(due_date).strftime(DATE_FORMAT)
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 = Date.parse(due_date).strftime(DATE_FORMAT)
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
def process_project(project_name, tasks, my_known_tasks)
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.strip == "" # 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|
puts "We have [#{task_name}]"
# task_name.strip!
next if task_name.strip == "" # 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
def get_no_project_tasks
workspaces = get_workspaces
workspaces.each_pair do |workspace_id, workspace_name|
project_name = "No-Project: #{workspace_name}"
%x{osascript -e '#{project_osascript(project_name)}'}
puts "Tasks for No Project in #{workspace_name}..."
tasks = get_tasks_in_no_project(workspace_id)
# Get my known tasks
result = %x{osascript -e '#{my_tasks_osascript(project_name)}'}
my_known_tasks = result.strip.split('|')
process_project(project_name, tasks, my_known_tasks)
end
end
def get_project_tasks
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.rstrip == '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('|')
process_project(project_name, tasks, my_known_tasks)
end
end
# -----------------------------------------------------------------------
# MAIN LOOP
# -----------------------------------------------------------------------
def run
get_no_project_tasks
get_project_tasks
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()
@zurdors
Copy link

zurdors commented Aug 5, 2017

Hi, Hiltmon.

I have one problem. I am getting the following error, during the execution of merge_asana_into_omnifocus.rb:

/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/net/http.rb:921:in `connect': Connection reset by peer - SSL_connect (Errno::ECONNRESET)
	from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/net/http.rb:921:in `block in connect'
	from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/timeout.rb:52:in `timeout'
	from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/net/http.rb:921:in `connect'
	from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/net/http.rb:862:in `do_start'
	from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/net/http.rb:851:in `start'
	from /Users/tlon-pro/Documents/bin/merge_asana_into_omnifocus.rb:41:in `get_json_data'
	from /Users/tlon-pro/Documents/bin/merge_asana_into_omnifocus.rb:59:in `get_workspaces'
	from /Users/tlon-pro/Documents/bin/merge_asana_into_omnifocus.rb:440:in `get_no_project_tasks'
	from /Users/tlon-pro/Documents/bin/merge_asana_into_omnifocus.rb:491:in `run'
	from /Users/tlon-pro/Documents/bin/merge_asana_into_omnifocus.rb:504:in `<main>'

Can you help me, please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment