blake.app

Awarding a Discourse badge from external data

Just for fun, every time I go to the gym I would like to award myself a workout badge. This is the final post in this little series, please checkout the previous posts for some more details.

In this post I’ll be creating a ruby file containing the code to check my online Gold’s Gym account, and code to talk to my Discourse instance and award myself a badge if I went to the gym that day.

Then I’ll create a cron job on my server to run daily that will call my ruby script.

One issue I realized when setting up the cron job is that my server is in UTC time, but I want the script to run at the end of the day Mountain Time. At first I thought I would have to handle this in the cron job somehow, but actually I can specify the date I pass to Gold’s Gym in ruby based on a specific timezone using the tzinfo gem.

@tz = TZInfo::Timezone.get('US/Mountain')
Time.now.getlocal(@tz.current_period.offset.utc_total_offset)

The golds check-in ruby script

For the golds check-in ruby script I setup two classes, GoldsCheckin and DiscourseBadge. At the bottom of the script I run golds_checkin.fetch_checkins and if I have a checkin for that day I assign myself a badge with discourse.grant_workout_badge:

require 'yaml'
require 'net/http'
require 'http-cookie'
require 'cgi'
require 'json'
require 'tzinfo'
require 'discourse_api'

class GoldsCheckin
  def initialize
    @config = YAML.load_file('config.yml')
    @jar = HTTP::CookieJar.new
    @tz = TZInfo::Timezone.get('US/Mountain')
  end

  def login
    uri = URI('https://mico.myiclubonline.com/iclub/j_spring_security_check')

    Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
      req = Net::HTTP::Post.new uri
      req.form_data = { "j_username": @config['username'], "j_password": @config['password'] }
      res = http.request req
      res.get_fields('Set-Cookie').each do |value|
        @jar.parse(value, req.uri)
      end
    end
  end

  def fetch_checkins
    today = Time.now.getlocal(@tz.current_period.offset.utc_total_offset)
    low_date = CGI::escape(today.strftime("%m/%d/%Y"))
    high_date = CGI::escape(today.strftime("%m/%d/%Y"))
    uri = URI("https://mico.myiclubonline.com/iclub/account/checkInHistory.htm?lowDate=#{low_date}&highDate=#{high_date}")

    Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
      req = Net::HTTP::Get.new(uri)
      req['Cookie'] = HTTP::Cookie.cookie_value(@jar.cookies(uri))
      res = http.request req
      res.body
    end
  end

end

class DiscourseBadge
  def initialize
    @config = YAML.load_file('discourse_config.yml')
    @client = DiscourseApi::Client.new(@config['host'])
    @client.api_key = @config['api_key']
    @client.api_username = @config['api_username']
  end

  def grant_workout_badge
    badge = {
      badge_id: 104,
      username: 'blake'
    }
    @client.grant_user_badge(badge)
  end
end

golds_checkin = GoldsCheckin.new
discourse = DiscourseBadge.new

golds_checkin.login
response = golds_checkin.fetch_checkins

json = JSON.parse(response)
if json.count > 0
  discourse.grant_workout_badge
end

Create a script to call the ruby app

Now that we have our little ruby app let’s create a bash script that we can easily call from our cron job that will call our ruby app.

Create a new file in /usr/bin and call it workout with the following contents:

#!/bin/bash

cd /golds_checkin && ruby app.rb

Now make sure our bash script is executable with:

chmod +x /usr/bin/workout

Create a cron job:

Now to finish it all off let’s create a cron job that will call our bash script. To create/edit a cron job run crontab -e:

0 5 * * * /usr/bin/workout

This should run our script once per day at 5 AM UTC, which will actually be 11:00 PM MST (the previous day). Usually I don’t go to gym that late, but this will capture if I ever go to gym in the evening after work.