JeanCarl's Adventures

Project 12: Hackathon Finder with Eventbrite

July 07, 2015 |

I’m often asked where I hear about upcoming hackathons. There are quite a few places I hear about hackathons, including at hackathons themself. But there is one website that often times becomes the funnel to locate them, Eventbrite.

Eventbrite is used by many events to manage RSVPs. In order to register to attend a hackathon, you RSVP at the hackathon’s Eventbrite event page. A search for hackathon on Eventbrite brings up several this month just in the Bay Area.

For my twelfth project in my 15 projects in 30 days, I’m going to use Eventbrite’s API to search for hackathons nearby and send an email with the list of upcoming events.

Eventbrite

To use the Eventbrite API, register an app to get a Client ID and Client Secret.

Photo

Photo

Copy the Client ID and Client Secret into the app.js file.

SendGrid

I use SendGrid to send emails with the hackathon events. You can find instructions on how to set up SendGrid in Project 5.

Setup

There are three files for this project. app.js is the Node.js app that makes API calls, serves the web interface, and runs a callback at a set interval to process any email subscriptions. hackathonfinder.js and index.html make up the AngularJS app for the web interface.

// Filename: app.js

var EVENTBRITE_CLIENT_ID = '';
var EVENTBRITE_CLIENT_SECRET = '';
var SENDGRID_USERNAME = '';
var SENDGRID_FROM = '';
var SENDGRID_PASSWORD = '';
var ADDRESS = '';
var PORT = 8080;
var CHECK_INTERVAL = 60000; // How often (in milliconds) to check for subscriptions to process.
var EMAIL_INTERVAL = 24*60*60000; // How many milliseconds between emails?

var express = require('express');
var bodyParser = require('body-parser');
var url = require('url');
var session = require('express-session');
var cookieParser = require('cookie-parser');
var request = require('request');
var sendgrid = require('sendgrid')(SENDGRID_USERNAME, SENDGRID_PASSWORD);

var mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/test');

var app = express();

var Subscription = mongoose.model('Subscriptions', {
  email: String,
  token: String,
  location: String,
  next: Number
});

app.use(bodyParser.json());
app.use(cookieParser());
app.use(session({
  secret: '1234567890QWERTY'
}))

function searchEvents(params, callback) {
  request({
      url: 'https://www.eventbriteapi.com/v3/events/search/',
      qs: params,
      method: 'GET',
    },
    function(err, response, body) {
      if(err || response.statusCode != 200) {
        callback('Unable to authenticate with Eventbrite.');
        return;
      } else {
        var js = JSON.parse(body);
        var events = [];

        for(var i in js.events)
        {
          events.push({
            title: js.events[i].name.text, 
            start: js.events[i].start.local, 
            end: js.events[i].end.local,
            url: js.events[i].url
          });
        }

        callback(null, events);
      }
  });
}

function sendEmail(email, subject, content)
{
  try {
    sendgrid.send({
      to: email,
      from: SENDGRID_FROM,
      subject: subject,
      text: content
    }, function(err, json) {
      if(err) 
        console.log(err);
    });
  } catch(e) {
    console.log(e);
  }
}

app.get('/oauth', function(req, res) {
  res.redirect('https://www.eventbrite.com/oauth/authorize?response_type=code&client_id='+EVENTBRITE_CLIENT_ID);
});

app.get('/oauth_callback', function(req, res) {
  var query = url.parse(req.url, true).query;

  req.session.access_token = query.code;

  var form = {
    client_id: EVENTBRITE_CLIENT_ID,
    code: query.code,
    client_secret: EVENTBRITE_CLIENT_SECRET,
    grant_type: 'authorization_code'
  }

  request({
      url: 'https://www.eventbrite.com/oauth/token',
      headers: {
        'Content-type': 'application/x-www-form-urlencoded'
      },
      form: form,
      method: 'POST',
    },
    function(err, response, body) {
      if(err || response.statusCode != 200) {
        res.send('Unable to authenticate with Eventbrite.');
      } else {
        var js = JSON.parse(body);

        req.session.access_token = js.access_token;
        res.redirect('/');
      }
  });
});

app.get('/api/me', function(req, res) {
  if(!req.session.access_token) {
    res.send({error: 'Not logged in'});
    return;
  }

  request({
      url: 'https://www.eventbriteapi.com/v3/users/me/?token='+req.session.access_token,
      method: 'GET',
    },
    function(err, response, body) {
      if(err || response.statusCode != 200) {
        res.send('Unable to get user info with Eventbrite.');
      } else {
        var js = JSON.parse(body);

        var userInfo = {};
        for(var i in js.emails) {
          if(js.emails[i].primary) {
            userInfo.email = js.emails[i].email;
          }
        }

        userInfo.name = js.name;
        res.send(userInfo);
      }
  });
});

app.get('/logout', function(req, res) {
  req.session.destroy();
  res.redirect('/');
});

app.get('/api/events', function(req, res) {
  if(!req.session.access_token) {
    res.send({error: 'Not logged in'});
    return;
  }

  var query = url.parse(req.url, true).query;
  var location = query.location;

  var qs = {
    token: req.session.access_token,
    q: 'hackathon',
    'location.address': location,
    'location.within': '75mi',
    sort_by: 'date',
    price: 'free'
    //'date_created.keyword': 'this_week'
  };

  searchEvents(qs, function(err, events) {
    if(err) {
      res.send({error: 'Unable to authenticate with Eventbrite.'});
      return;
    } else {
      res.send(events);
    }
  });
  
})

app.post('/api/subscribe', function(req, res) {
  if(!req.session.access_token) {
    res.send({error: 'Not logged in'});
    return;
  }

  var timeNow = new Date();
  
  Subscription.create({
    email: req.body.email, 
    location: req.body.location, 
    next: timeNow.getTime(),
    token: req.session.access_token
  }, function(error, doc) {
    if(error) {
      res.send({error: 'Unable to subscribe'});
      return;
    }

    res.send({location: doc.location});
  });
});

app.get('/unsubscribe', function(req, res) {
  var query = url.parse(req.url, true).query;

  Subscription.remove({_id: query.subscriptionid}, function(error, numAffected) {
    if(error || numAffected.result.ok != 1) {
      res.send('Could not unsubscribe');
    } else {
      res.send('Unsubscribed');
    }
  });
});

setInterval(function() {
  var qs = {
    q: 'hackathon',
    sort_by: 'date',
    price: 'free'
    // 'date_created.keyword': 'this_week'    
  };

  var timeNow = new Date();

  Subscription.find({next: {$lt: timeNow.getTime()}}, function(error, subscriptions) {
    for(var i in subscriptions) {
      qs['location.address'] = subscriptions[i].location;
      qs['location.within'] = '75mi';
      qs.token = subscriptions[i].token;

      var sub = subscriptions[i];
      searchEvents(qs, function(err, events) {
        if(err) {
          console.log(err);
          return;
        }

        Subscription.update({_id: sub._id}, {next: timeNow.getTime()+EMAIL_INTERVAL}, function(err, numAffected) {
          if(err) {
            console.log(err);
            return;
          }          
        });

        if(events.length == 0) {
          return;
        }

        var content = '';
        for(var i in events) {
          content += events[i].title+"\n"+events[i].start.substring(0, 10)+" - "+events[i].end.substring(0, 10)+"\n"+events[i].url+"\n\n";
        }

        content += 'Unsubscribe at: '+ADDRESS+':'+PORT+'/unsubscribe?subscriptionid='+sub._id;

        sendEmail(sub.email, 'New hackathons', content);
      }); 
    }
  });
}, CHECK_INTERVAL);

app.listen(PORT);

app.use(express.static(__dirname + '/public'));

console.log('Application listening on port '+PORT);
// Filename: public/hackathonfinder.js

angular.module('HackathonFinderApp', [])
.controller('HackathonFinderCtrl', ['$scope', '$http', function($scope, $http) {

  $http.get('/api/me').then(function(response) {
    $scope.doneLoading = true;
    $scope.location = '';

    if(response.data.error) {
      $scope.isLoggedIn = false;
      return;
    }

    $scope.email = response.data.email;
    $scope.name = response.data.name;
    $scope.isLoggedIn = true;
  });

  $scope.findEvents = function() {
    $http.get('/api/events?location='+$scope.location).then(function(response) {
      if(response.data.error) {
        alert('Error: '+response.data.error);
        return;
      }

      $scope.events = response.data;
    });
  }

  $scope.subscribe = function() {
    $http.post('/api/subscribe', {location: $scope.location, email: $scope.email}).then(function(response) {
      if(response.data.error) {
        alert('Error: '+response.data.error);
        return;
      }

      alert('You are now subscribed to hackathons near '+$scope.location);
    });
  }
}]);
<!-- Filename: public/index.html --> 
<html ng-app="HackathonFinderApp">
<head>
  <title>Hackathon Finder</title>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
  <script src="hackathonfinder.js"></script>
</head>
<body ng-controller="HackathonFinderCtrl" style="font-family:Arial">
  ## Hackathon Finder

  <div ng-hide="doneLoading">
    Loading...
  </div>

  <div ng-show="isLoggedIn === false">
    <a href="/oauth">Login with Eventbrite</a>
  </div>

  <div ng-show="isLoggedIn">
    Find hackathons near: <input type="text" ng-model="location" /><input type="button" ng-click="findEvents()" ng-disabled="location.length == 0" value="Search" /> <input type="button" ng-click="subscribe()" ng-disabled="location.length == 0" value="Subscribe" />

    <div ng-repeat="event in events">
      <h2><a href="{{event.url}}" target="_blank">{{event.title}}</a></h2>

      <p>{{event.description}}</p>

      <p>{{event.start | date:"MM/dd"}} - {{event.end | date:"MM/dd"}}</p>
    </div>
  </div>
</body>
</html>
// package.json
{
  "name": "hackathon-finder",
  "description": "Hackathon Finder application for Node.js",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "*",
    "body-parser": "*",
    "url": "*",
    "express-session": "*",
    "cookie-parser": "*",
    "request": "*",
    "mongoose": "*",
    "sendgrid": "*"
  }
}

This project uses Node.js and MongoDB, which I explain how to setup in another project.

To install the Node.js module dependencies, run the command:

npm install

And to start the app, run the command:

nodejs app.js

Hackathon Finder

Hackathon Finder uses the OAuth protocol to get information about the user. The app sends the user off to Eventbrite to authorize the app to access their Eventbrite account.

Photo

When the user returns to the app, Hackathon Finder stores the access token in the request session. This access token is used for subsequent API calls to Eventbrite. The AngularJS app will makes a call to /api/me to get the user’s email address, which is stored for use in a little bit.

We can now search for upcoming events. Enter a location, such as Mountain View, CA and click on Search. Hackathon Finder will fetch upcoming events on Eventbrite that have the keyword hackathon and which are near this location.

Photo

This is great, but I might forget to come here and search for upcoming events. Wouldn’t it be nice to get an email with the list of results? Click on the Subscribe button. You’ll get a confirmation when the subscription is successful.

Photo

The next time Hackathon Finder checks for subscriptions, it will find this subscription and perform a search for hackathon events nearby the location. It will send an email using SendGrid.

Photo

For this project, I added a link to unsubscribe from the notification inside the email at the bottom.

I want to step back and explain where the email address of the subscription came from. When the page first loaded, the AngularJS app made a request to /api/me endpoint which got the Eventbrite user info. This user info includes one or more email addresses. The primary email address is used as the recipient address for the subscription.

That’s it for this project. Here are some things you can do to extend this project:

  • I've commented out the line 'date_created.keyword': 'this_week'. This would only look for events created this week. Add the capability to be notified only about the newest events.
  • The web interface needs an event description and the user's name. Are you up for the challenge?
  • Instead of emailing the new events, how about sending text messages using Twilio, Tropo, or Nexmo.
  • You can subscribe to multiple locations, one at a time. Streamline this option by combining multiple locations into one notification.
  • If there are multiple email addresses from Eventbrite User API, ask the user which email address should be used to subscribe to the emails.
  • Technically the email address for these subscriptions is stored in the JavaScript. Store it in the session and verify the chosen address is in the list of email address from Eventbrite. This way you can verify that the user "owns" this email address and it adds a little more security.

Source Code

You can find the repo on GitHub.

Post Mortem

The Eventbrite API is pretty interesting and easy to use. However, the location.address and location.within fields aren’t quite so friendly. Ideally the name of this field would be locationaddress and locationwithin for it to be accessible in object notation.

15 Projects in 30 Days Challenge

This blog post is part of my 15 projects in 30 days challenge. I’m hacking together 15 projects with different APIs, services, and technologies that I’ve had little to no exposure to. If my code isn’t completely efficient or accurate, please understand it isn’t meant to be complete and bulletproof. When something is left out, I try to mention it. Reach out to me and kindly teach me if I go towards the dark side. ?

This challenge serves a couple of purposes. First, I’ve always enjoyed hacking new things together and using APIs. And I haven’t had the chance (more like a reason) to dive in head first with things like AngularJS, Node.js, MongoDB, and Eventbrite. This project demonstrated AngularJS, Node.js, MongoDB, and Eventbrite.