Voting at API Hack Day: A Kynetx App


API Hack Day Results

Sam Curren and Brad Hitze were at API Hack Day yesterday. In a fit of meta hacking, Sam created a voting app that was, at the same time, an app competing in the hackathon and the app recording the votes for who won. The API Hack Day results we computed and communicated in real time using a twitter account from votes made via phone or SMS. Sam's app combined Twilio, Twitter, and a Google spreadsheet.

Sam reports that a few people gamed it by provisioning and then dropping telephone numbers, but that's OK. It was still fun. Next time he'll take more precautions in hackathon voting (after all it is a hackathon).

This kind of application is a natural in KRL because an event-driven architecture is a no-brainer in this situation. As you'll see in the code below, modeling the interactions by describing event patterns in the select expressions and then writing responses to them provides a readable and easily maintainable application. The flow can be a little hard to visualize if you're not used to thinking in terms of events, but is pretty simple.

Sam defines seven rules in two parallel event chains, one for SMS and one for voice. If the voter is voting by SMS, then the inboundsms is fired and through explicit events leads to recordvote and then to tweetit. If the voter phones in callstart is fired leading to getvote, phonevote, and finally, tweetit:

People's Choice App flow

In both paths through the app, the end result is that the current winner is tweeted. The Google spreadsheet is just doing a form post for updates, which has some limitations in terms of update frequency imposed by Google.

Here's Sam's code, with keys redacted:

ruleset a8x76 {
  meta {
    name "PeoplesChoice"
    description <<
      votes for apihackday
    >>
    author "Sam Curren"
    logging on
    key twitter {
       *redacted*
    }
  }
 
  global {
    dataset winningdemo <- 
     "https://spreadsheets.google.com/pub?key=*redacted*&single=true&gid=4&range=D2&output=txt" 
     cachable for 1 second;
  }
 
  rule inboundsms is active {
    select when twilio sms
    pre {
      messagetext = event:param("Body");
      sender = event:param("From");
      applist = messagetext.extract(re/(\\d+)/);
    }
    if(applist.length() > 0) then {
        noop();
    }
    fired{
      raise explicit event recordvote with 
         appvote = applist.head() and 
         voter = sender;
    } else {
      raise explicit event help;
    }
  }
      
  rule help is active {
    select when explicit help
    twilio:sms("Vote for your APIHackDay demo x by texting 'vote x'");
  }
    
  rule recordvote is active {
    select when explicit recordvote
    pre {
      voter = event:param("voter");
      appvote = event:param("appvote");
    }
    {
      http:post("https://spreadsheets.google.com/formResponse?formkey=*redacted*") 
       with params = {"entry.0.single":voter, 
                      "entry.1.single":appvote, 
                      "submit":"Submit", 
                      "pageNumber":"0",
                      "backupCache":""};
      twilio:sms("You voted for app #{appvote}. Only your last vote counts. Thanks! (Powered by Kynetx + Twilio)");
    }
    fired {
      raise explicit event tweet;
    }
  }
        
  rule callstart is active {
    select when twilio callstart
    {
      twilio:say("Thank you for coming to API Hack Day");
      twilio:pause(1);
    }
    fired {
      raise explicit event getvote;
    }
  }
      
  rule getvote is active{
    select when explicit getvote or twilio getvote
    {
      twilio:gather_start("phonevote");
      twilio:say("Enter the number of your favorite app, followed by the pound sign.");
      twilio:gather_stop();
      twilio:redirect("getvote");
    }
  }
      
  rule phonevote is active {
    select when twilio phonevote
    pre {
      appvote = event:param("Digits");
      voter = event:param("Caller");
    }
      {
      http:post("https://spreadsheets.google.com/formResponse?formkey=*redacted*") 
         with params = {"entry.0.single":voter, 
                        "entry.1.single":appvote, 
                        "submit":"Submit", 
                        "pageNumber":"0",
                        "backupCache":""};
      twilio:say("Your vote for app #{appvote} has been recorded. Only your last vote counts. Thank You!");
    }
      fired {
        raise explicit event tweet;
      }
  }
      
  rule tweetit is active {
    select when explicit tweet or pageview ".*"
        pre {
          winner = winningdemo;
          tweet = "Current Voting Leader is App #{winner}! #apihackday";
        }
      twitter:update(tweet);
 
  }
}

The code makes use of the Kynetx webhook endpoint to tie to Twilio. Note that the Consumer and OAuth keys for the Twitter link were redacted so it may not be obvious how the connection to Twitter is made from the source. Sam's going to do a more detailed analysis of the code and describe some of the trickier aspects (like what all the Twilio actions do and how they work). When he does, I'll update this post to point to it.

All in all, not a lot of code to intergrate a couple of APIs and a telephony service. Especially given that it's supporting SMS and voice. A dedicated Google spreadsheet integration in KNS would make this cleaner and improve the app's performance. I'm very impressed with what Sam was able to pull off in a few hours.