www.howardism.org
Babblings of an aging geek in love with the Absurd, his family, and his own hubris.... oh, and Lisp.

Connecting to Habitica


Me, pretending to be my witchie avatar.

Lately, I’ve moved my “Task List” over to Habitica for a couple of reasons:

The idea that tasks completed on Monday, are unchecked and ready to do again on Tuesday is something I haven’t seen with any other task tracker, not to mention the drops of pets and blingy costumes.

One feature I wish it had was the recurring tasks that are longer than a week. I can have a task of “Get Ready to Teach my Class” appear every Tuesday, but I can’t have “Clean the Coffee Pot” re-show at the first of each month, nor “Schedule Teeth Scraping” every six months.

Since the Habitica web site is a single page app, they have a complete REST API for everything. This means it is straight-forward to write a program to compensate for many features. Since one of the members of my party was trying to level up on Ruby, I thought I’d write a Ruby script that could run daily as a cronjob.

Couple of rules:

Data Structures

Given the rules listed above, it doesn’t take much to create a list of recurring events. Each event, if represented by a map of specific fields, I can just pass them along.

For instance, a day of the month could be represented by an integer:

{ text: "Clean Coffee Pot", date: 1 }

Events that happen once a year should just have a parse-able date:

{ text: "Talk like a Pirate", date: "Sep 19" }

Events that happen multiple times a year could have multiple dates in a string:

{ text: "Replace Air Filter", date: '20-May|1-Oct',
  tags: ["299ee46d-025a-4dab-87e6-7a4562eaffe1"]}

Found out the hard way that the tags need to be specify by ID. We’ll want to have a way to get those by name.

My blood donations that occur every eight weeks need something more complicated, so perhaps we can store an expression. In Ruby that could be a simple lambda:

{ text: "Blood Donation",
  date: lambda {|d| d.cweek % 8 == 2 and d.tuesday?},
  priority: 1.5, attribute: 'con',
  checklist: [{'text': 'Drink a cup of tea'},
              {'text': 'Drink another cup of tea'},
              {'text': 'Fill out RapidPass'},
              {'text': 'Do not forget lunch'}]}

A Medium difficulty is specified by the priority field of 1.5. To create a task with a list of sub-tasks, create a checklist section.

While I might want to eventually put these tasks in a separate file, for now, let’s just create a global variable, @cyclic_tasks:

@cyclic_tasks = [
  # Each entry is a map ...
]

When to Create

A function that could take a value from our large selection of date specifications and return true if the event should be created for a given date:

def task_due?(now, date)
  if date.is_a? String
    # Dates as string would be something like Jan-13
    dates = date.split(/ *\| */).collect { |d| Date.parse(d) }
    dates.any? { |d| now.yday == d.yday }
  elsif date.is_a? Integer
    now.mday == date
  elsif date.is_a? Proc
    date.call(now)
  else  # Assume `date` is an actual Date object
    now.month == date.month and now.mon == date.mon
  end
end

Let’s make sure this works as expected:

require 'date'

now = Date.new(2017, 02, 05)

task_due?(now, 5)
task_due?(now, 'Feb-05')
task_due?(now, 'Jan 10 | Feb 5 | Mar 23')
task_due?(now, lambda {|d| d.yday == 36 })

Yeah, we should move this into a Spec test file.

What to Create

Loop through the recurring events data structure, @cyclic_tasks and give us a list of what tasks should be created:

def todays(date, tasks)
  tasks.select do |task|
    task_due?(date, task[:date])
  end
end

What dates will we want to generate recurring tasks?

theFirst = Date.new(2017, 1, 1)
365.times do |d|
  date = theFirst + d
  tasks = todays(date, @cyclic_tasks)
  tasks.each do |task|
     p "#{task[:text]} on #{date.asctime}"
  end
end

That bit of code will verify the tasks you want to create. Now, about creating those todos…

Connecting to the Web Service

While people have created a nice Ruby library for Habitica, the API is a so straight-forward and simple, we can access easily with the standard HTTPClient library. Create a new client:

require 'httpclient'

@http = HTTPClient.new

And then, we can create a couple of helper functions for GET and POST calls: Note, these require your User ID and the API Key:

@habitica_url = 'https://habitica.com/api/v3'

def hab_get(path, query = nil)
  req = @http.get "#{habitica_url}/#{path}", query,
                  'Content-Type' => 'application/json',
                  'x-api-user' => @user_id, 'x-api-key' => @api_key
  JSON.parse(req.body)['data']
end

def hab_post(path, data = nil)
  @http.post "#{habitica_url}/#{path}", body=data,
                   'x-api-user' => @user_id, 'x-api-key' => @api_key
end

Habitica-Specific Functions

Getting the list of tasks as an array of hash maps in trivial:

def get_tasks
  hab_get('tasks/user')
end

To filter the list to only the TODO type of tasks, we use select:

def get_todos
  only_todos = lambda {|t| t['type'] == 'todo'}
  get_tasks.select(&only_todos)
end

Since one of our rules was not to create a task if it already exists, we can use the any? function to check the list:

def task_exists?(task)
  get_todos.any? { |t| t['text'] == task[:text]}
end

Creating a task from one of the hashmap data structures above, is equally easy, as long as I remove the :date entry:

def create_task(details)
  details.delete(:date)
  hab_post('tasks/user', details)
end

To make sure what I create is a TODO, we just need to add the :type:

def create_todo(details)
  details[:type] = 'todo'
  create_task(details)
end

Final step is to create a main part where we iterate over our tasks, picking out anything that matches today that does not already exist:

todays(Date.today, @cyclic_tasks).each do |task|
  create_todo(task) if not task_exists?(task)
end

Done!

Slight improvement … creating a function to download my tags, and create an association of the tags name with its ID, allows me to write, tags()['morning']:

def tags()
  tag_map = hab_get('tags').collect do |tag|
    [tag['name'], tag['id']]
  end
  Hash[tag_map]
end

If I memoize that function in a global variable, @tags, then I can easily specify those tags in my original data structure.

Summary

This has been just a bit of exploration for connecting a simple Ruby script to your Habitica account. For me, I run this script daily, as part of a cron job every morning before I wake, so my todos are ready for me to complete.