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.