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.