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:
- Nice integration between desktop and mobile
- Recurring tasks re-show each day/week
- Quite fun, and surprisingly motivating
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:
- If the task already exists, it is not created … this means that I won’t have a TODO list with unfinished duplicates.
- Script must run daily and if the script misses to set a task on its date, it will have to wait until the next cycle … I’m willing to live with such imperfections.
- While tasks tend to be repeat on either monthly or yearly cycles, some, like my blood donation scheduling, are more complicated, like every 8 weeks.
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.