How to score The Daily Show tickets using Twilio and Golang

I’m a big fan of Comedy Central’s The Daily Show previously hosted by John Stuart who handed over to Trevor Noah in 2015. The show is recorded in New York City in front of a live studio audience. Tickets to the show are free but limited. And therefore they are hard to get. There are two types of tickets:

  • GENERAL GUARANTEED tickets which guarantee a seat in the audience if you arrive in time, and
  • GENERAL - ENTRY NOT GUARANTEED tickets that might get you in when you wait in line next to the studio. People holding this ticket type may be invited to fill in remaining seats.

On the ticket website is a little calendar in the top right corner. Dates on which the show will be airing become available in chunks from time to time. Over the short period of time I monitored this I was not able to find a reoccurring pattern from which I could predict future ticket rounds. If I wanted to get lucky with a ticket on a specific date I would have to regularly check the ticket website. I believe humans should delegate repetitive tasks like this one to a computer.

A small program would do the job of

  • checking the website regularly for ticket availability at specific dates and
  • notifying me once tickets are available.

Checking the Dates

To get an idea of how the website was structured and how I could detect changes in the availability of tickets I took a look a the source code. 👀 The site is structured mostly using HTML.

The calendar, however, is rendered via JavaScript.

var dates_avail = {
  "2018-02-20":"style1",
  "2018-02-21":"style1",
  "2018-02-22":"style1",
  ✂️
  "2018-03-28":"style1",
  "2018-03-29":"style1"
};

Luckily, the available dates are stored in an object in the source. Furthermore, to find a particular date I simply needed to search for it in the right notation, for example "2018-02-20":"style1". That string was not matching anything else but the ticket calendar if, and only if, tickets were available on that date. That was so much easier than I thought!

So the checking part of my program was down to simply fetching the website’s source and then running a substring search over the received data.

My weapon language of choice, as usual: Golang. I used the http package to fetch the website’s source:

ticketWebsiteURL := "https://www.showclix.com/event/TheDailyShowwithTrevorNoah"
response, err := http.Get(ticketWebsiteURL)
if err != nil {
  ️
}
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
  ️
}

For the substring search, I started with first defining the dates at which I would be in New York and have enough spare time to make it to the studio. After all, there is no use blocking a seat that other would be happy to have.

triggerDates := []string{"2018-02-20", "2018-02-21"}

Ranging over the triggerDates I defined the search pattern for each date individually and then used the Contains() function of the strings package.

for _, td := range triggerDates {
  search := "\"" + td + "\":\"style1\""
  if strings.Contains(string(contents), search) {
    // Found it! Let's send a notification.
    ️
  }
}
response.Body.Close()

That was the easier part.

Notification via SMS

I spend most of my life in the Central European Time (CET) timezone but the show is recorded in the Eastern Standard Time (EST) timezone. I assumed the tickets would, therefore, become available sometime during EST office hours, at which I might be asleep.

I needed a notification method that would safely wake me up but not interfere with the quiet hours settings of my phone. I could have used the Pagerduty API but somehow I found this to be overkill. Overkill, because I have an alert escalation configuration at Pagerduty that truly wakes me up. And my girlfriend, much to her excitement. #not

A communication channel that has been almost forgotten is the good old Short Messaging Service (SMS). I get so few SMS messages, that receiving one can hardly disturb my usual flow. With SMS to the rescue, I configured my phone to notify for SMS even during quiet hours. Now I only need to make my program send SMS. That may sound tricky, but in the age of Cloud, this is just a web service away. I headed over to Twilio to register a mobile phone number that I would use as message source.

Here is how I crafted the message in Go:

msgData := url.Values{}
msgData.Set("To", "+00-MY-PHONE-NUMBER")
msgData.Set("From", "+00-MY-TWILIO-NUMBER")
msgData.Set("Body",
  "Tickets available now ("+info+")! Visit "+ticketWebsiteURL)

Sending the text was then as easy as creating a REST call:

req, _ := http.NewRequest("POST", apiURL, &msgDataReader)
req.SetBasicAuth(accountSid, authToken)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

There are a few more lines to making a successful call to the Twilio API, of course. Scroll down for the full source.

Wait for it… Win!

With fetching and notifications being set up I just had to run the code. For this I copied it over to my jump host, a compute instance that is running 247 anyway and could use some additional load. 😉

04:35:33 waiting...
04:35:33 fetching...
04:50:34 waiting...
05:05:34 fetching...
05:05:35 sms sent!
exit status 1

One day, right after my early workout, the long-awaited for text arrived. I got lucky and scored two tickets for one of the dates I wanted. Hooray!

Greetings from The Daily Show with Trevor Noah everyone!

Source Code

Here’s the full source code FYI. Enjoy the show! 🎥

package main

import (
  "encoding/json"
  "io/ioutil"
  "log"
  "net/http"
  "net/url"
  "strings"
  "time"
)

var (
  triggerDates     = []string{"2018-02-20", "2018-02-21"}
  ticketWebsiteURL = "https://www.showclix.com/event/TheDailyShowwithTrevorNoah"
)

func sendSMS(info string) {
  accountSid := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  authToken := "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
  apiURL := "https://api.twilio.com/2010-04-01/Accounts/" +
    accountSid + "/Messages.json"

  msgData := url.Values{}
  msgData.Set("To", "+00-YOUR-NUMBER")
  msgData.Set("From", "+00-YOUR-TWILIO-NUMBER")
  msgData.Set("Body",
    "Tickets available now ("+info+")! Visit "+ticketWebsiteURL)
  msgDataReader := *strings.NewReader(msgData.Encode())

  client := &http.Client{}
  req, _ := http.NewRequest("POST", apiURL, &msgDataReader)
  req.SetBasicAuth(accountSid, authToken)
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

  resp, _ := client.Do(req)
  // Let's be very generous with the status code. We want those tickets!
  if resp.StatusCode >= 200 && resp.StatusCode < 300 {
    var data map[string]interface{}
    decoder := json.NewDecoder(resp.Body)
    err := decoder.Decode(&data)
    if err == nil {
      log.Printf("twilio: decode json: %v", err)
    }
  } else {
    log.Printf("twilio: status code: %v", resp.Status)
  }
}

func main() {
  for {
    // Behave! Wait 15 minutes to not overload the site or spam their logs.
    log.Printf("waiting...\n")
    time.Sleep(15 * time.Minute)

    // Fetch the website.
    log.Printf("fetching...\n")
    response, err := http.Get(ticketWebsiteURL)
    if err != nil {
      log.Printf("get: %v", err)
      continue
    }
    contents, err := ioutil.ReadAll(response.Body)
    if err != nil {
      log.Printf("read body: %v", err)
      continue
    }
    // Look for the trigger dates in the source code.s
    for _, td := range triggerDates {
      search := "\"" + td + "\":\"style1\""
      if strings.Contains(string(contents), search) {
        // Found it! Let's send a notification.
        sendSMS(td)
        log.Fatalf("sms sent!")
      }
    }
    response.Body.Close()
  }
}