You are on page 1of 58

Build a simple Twitter

clone with Ruby

Monday, September 20, 2010


Today’s lesson
Features of a simple micro-blogging site

Authentication with Facebook Graph API


OAuth 2.0

Code walkthrough

http://github.com/sausheong/chirpy

Monday, September 20, 2010


Features

Monday, September 20, 2010


Authentication via Facebook OAuth 2.0

Users can post a text message called a chirp


in his own chirp feed

Users can follow and unfollow any other user

Users’ feeds are added to their followers’


feeds

Users can reply to chirps or re-chirp

Monday, September 20, 2010


Monday, September 20, 2010
Monday, September 20, 2010
Monday, September 20, 2010
Facebook Graph API
OAuth 2.0

Monday, September 20, 2010


Monday, September 20, 2010
Monday, September 20, 2010
Monday, September 20, 2010
Monday, September 20, 2010
Monday, September 20, 2010
Code walkthrough

Monday, September 20, 2010


Model

Monday, September 20, 2010


followers

1 n
n
1 has 1
follows User Session
1
1

has

Chirp

Monday, September 20, 2010


class Session
include DataMapper::Resource
property :id, Serial
property :uuid, String, :length => 255
belongs_to :user
end

class Friendship
include DataMapper::Resource

belongs_to :source, 'User', :key => true


belongs_to :target, 'User', :key => true
end

Monday, September 20, 2010


class User
include DataMapper::Resource

property :id, Serial


property :name, String, :length => 255
property :photo_url, String
property :facebook_id, String
property :chirpy_id, String

has n, :chirps
has 1, :session

has n, :follower_relations, 'Friendship', :child_key => [ :source_id ]


has n, :follows_relations, 'Friendship', :child_key => [ :target_id ]
has n, :followers, self, :through => :follower_relations, :via => :target
has n, :follows, self, :through => :follows_relations, :via => :source

def chirp_feed
feed = follows.collect {|follow| follow.chirps}.flatten + chirps
feed.sort { |chirp1, chirp2| chirp2.created_at <=> chirp1.created_at}
end
end

Monday, September 20, 2010


class Chirp
include DataMapper::Resource

property :id, Serial


property :text, String, :length => 140
property :created_at, Time

belongs_to :user

before :save do
if starts_with?('follow ')
process_follow
else
process
end
end

Monday, September 20, 2010


def process
urls = self.text.scan(URL_REGEXP)
urls.each { |url|
tiny_url = open("http://tinyurl.com/api-create.php?url=#{url[0]}")
{|s| s.read}
self.text.sub!(url[0], "<a href='#{tiny_url}'>#{tiny_url}</a>")
}

ats = self.text.scan(AT_REGEXP)
ats.each { |at| self.text.sub!(at, "<a href='/#{at[2,at.length]}'>#
{at}</a>") }
end

def process_follow
user = User.first :chirpy_id => self.text.split[1]
user.followers << self.user
user.save
throw :halt # don't save this chirp
end

Monday, September 20, 2010


def starts_with?(prefix)
prefix = prefix.to_s
self.text[0, prefix.length] == prefix
end
end

URL_REGEXP = Regexp.new('\b ((https?|telnet|gopher|file|wais|


ftp) : [\w/#~:.?+=&%@!\-] +?) (?=[.:?\-] * (?: [^\w/#~:.?+=&%@!
\-]| $ ))', Regexp::EXTENDED)
AT_REGEXP = Regexp.new('\s@[\w.@_-]+', Regexp::EXTENDED)

Monday, September 20, 2010


Controller

Monday, September 20, 2010


%w(haml sinatra rack-flash json rest_client active_support dm-
core).each { |gem| require gem}
%w(config models helpers).each {|feature| require feature}

set :sessions, true


set :show_exceptions, false
use Rack::Flash

get '/' do
redirect '/home' if session[:id]
redirect '/login'
end

get '/login' do
haml :login, :layout => false
end

Monday, September 20, 2010


Facebook authentication

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

(3a) Facebook calls redirect URI with:


- code

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

(3a) Facebook calls redirect URI with: (3b) Facebook calls redirect URI with:
- code - error reason

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

(3a) Facebook calls redirect URI with: (3b) Facebook calls redirect URI with:
- code - error reason

(4) Go to /oauth/access_token with:


- client id
- redirect URI
- client secret
- code

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

(3a) Facebook calls redirect URI with: (3b) Facebook calls redirect URI with:
- code - error reason

(4) Go to /oauth/access_token with:


- client id
- redirect URI
- client secret
- code

(5) Facebook responds with:


- access token
- expiry

Monday, September 20, 2010


(1) Redirect user to /oauth/authorize with:
- client id
- redirect URI

(2) User authorizes (or not)

(3a) Facebook calls redirect URI with: (3b) Facebook calls redirect URI with:
- code - error reason

(4) Go to /oauth/access_token with:


- client id
- redirect URI
- client secret
- code

(5) Facebook responds with:


- access token
- expiry

(6) Call Graph API with access token

Monday, September 20, 2010


chirpy.rb
get '/login/facebook' do
facebook_oauth_authorize
end

1
helper.rb
def facebook_oauth_authorize
redirect "https://graph.facebook.com/oauth/authorize?client_id=" +
FACEBOOK_OAUTH_CLIENT_ID +
"&redirect_uri=" +
"http://#{env['HTTP_HOST']}/#{FACEBOOK_OAUTH_REDIRECT}"
end

Monday, September 20, 2010


chirpy.rb
get "/#{FACEBOOK_OAUTH_REDIRECT}" do
redirect_with_message '/login', params[:error_reason] if
params[:error_reason]
facebook_get_access_token(params[:code])
end

Monday, September 20, 2010


chirpy.rb
get "/#{FACEBOOK_OAUTH_REDIRECT}" do
redirect_with_message '/login', params[:error_reason] if
params[:error_reason]
facebook_get_access_token(params[:code])
end

3a

Monday, September 20, 2010


chirpy.rb 3b
get "/#{FACEBOOK_OAUTH_REDIRECT}" do
redirect_with_message '/login', params[:error_reason] if
params[:error_reason]
facebook_get_access_token(params[:code])
end

3a

Monday, September 20, 2010


helper.rb
def facebook_get_access_token(code)
oauth_url = "https://graph.facebook.com/oauth/access_token"
oauth_url << "?client_id=#{FACEBOOK_OAUTH_CLIENT_ID}"
oauth_url << "&redirect_uri=" + URI.escape("http://#{env['HTTP_HOST']}/#
{FACEBOOK_OAUTH_REDIRECT}")
oauth_url << "&client_secret=#{FACEBOOK_OAUTH_CLIENT_SECRET}"
oauth_url << "&code=#{URI.escape(code)}"

response = RestClient.get oauth_url


oauth = {}
response.split("&").each do |p| ps = p.split("="); oauth[ps[0]] = ps[1] end
user_object = get_user_from_facebook_with URI.escape(oauth['access_token'])
user = User.first_or_create :facebook_id => user_object['id']
user.name = user_object['name']
user.photo_url = "http://graph.facebook.com/#{user_object['id']}/picture"
user.chirpy_id = user_object['name'].gsub " ","-"
user.session = Session.new :uuid => oauth['access_token']
user.save
session[:id] = oauth['access_token']
session[:user] = user.id
redirect '/'
end

Monday, September 20, 2010


helper.rb
def facebook_get_access_token(code)
oauth_url = "https://graph.facebook.com/oauth/access_token"
oauth_url << "?client_id=#{FACEBOOK_OAUTH_CLIENT_ID}"
oauth_url << "&redirect_uri=" + URI.escape("http://#{env['HTTP_HOST']}/#
{FACEBOOK_OAUTH_REDIRECT}")
oauth_url << "&client_secret=#{FACEBOOK_OAUTH_CLIENT_SECRET}"
oauth_url << "&code=#{URI.escape(code)}"

response = RestClient.get oauth_url


4
oauth = {}
response.split("&").each do |p| ps = p.split("="); oauth[ps[0]] = ps[1] end
user_object = get_user_from_facebook_with URI.escape(oauth['access_token'])
user = User.first_or_create :facebook_id => user_object['id']
user.name = user_object['name']
user.photo_url = "http://graph.facebook.com/#{user_object['id']}/picture"
user.chirpy_id = user_object['name'].gsub " ","-"
user.session = Session.new :uuid => oauth['access_token']
user.save
session[:id] = oauth['access_token']
session[:user] = user.id
redirect '/'
end

Monday, September 20, 2010


helper.rb
def facebook_get_access_token(code)
oauth_url = "https://graph.facebook.com/oauth/access_token"
oauth_url << "?client_id=#{FACEBOOK_OAUTH_CLIENT_ID}"
oauth_url << "&redirect_uri=" + URI.escape("http://#{env['HTTP_HOST']}/#
{FACEBOOK_OAUTH_REDIRECT}")
oauth_url << "&client_secret=#{FACEBOOK_OAUTH_CLIENT_SECRET}"
oauth_url << "&code=#{URI.escape(code)}"

response = RestClient.get oauth_url


4 5
oauth = {}
response.split("&").each do |p| ps = p.split("="); oauth[ps[0]] = ps[1] end
user_object = get_user_from_facebook_with URI.escape(oauth['access_token'])
user = User.first_or_create :facebook_id => user_object['id']
user.name = user_object['name']
user.photo_url = "http://graph.facebook.com/#{user_object['id']}/picture"
user.chirpy_id = user_object['name'].gsub " ","-"
user.session = Session.new :uuid => oauth['access_token']
user.save
session[:id] = oauth['access_token']
session[:user] = user.id
redirect '/'
end

Monday, September 20, 2010


helper.rb
def facebook_get_access_token(code)
oauth_url = "https://graph.facebook.com/oauth/access_token"
oauth_url << "?client_id=#{FACEBOOK_OAUTH_CLIENT_ID}"
oauth_url << "&redirect_uri=" + URI.escape("http://#{env['HTTP_HOST']}/#
{FACEBOOK_OAUTH_REDIRECT}")
oauth_url << "&client_secret=#{FACEBOOK_OAUTH_CLIENT_SECRET}"
oauth_url << "&code=#{URI.escape(code)}"

response = RestClient.get oauth_url


4 5
oauth = {}
response.split("&").each do |p| ps = p.split("="); oauth[ps[0]] = ps[1] end
user_object = get_user_from_facebook_with URI.escape(oauth['access_token'])
user = User.first_or_create :facebook_id => user_object['id']
user.name = user_object['name']
user.photo_url = "http://graph.facebook.com/#{user_object['id']}/picture"
user.chirpy_id = user_object['name'].gsub " ","-"
user.session = Session.new :uuid => oauth['access_token']
user.save 6
session[:id] = oauth['access_token']
session[:user] = user.id
redirect '/'
end

Monday, September 20, 2010


helper.rb
def get_user_from_facebook_with(token)
JSON.parse RestClient.get "https://graph.facebook.com/me?
access_token=#{token}"
end

chirpy.rb
get '/logout' do
@user = User.get session[:user]
@user.session.destroy
session.clear
redirect '/'
end

Monday, September 20, 2010


Authorization

Monday, September 20, 2010


helper.rb
def require_login
if session[:id].nil?
redirect_with_message('/login', 'Please login first')
elsif Session.first(:uuid => session[:id]).nil?
session[:id] = nil
redirect_with_message('/login', 'Session has expired, please
log in again')
end
end

Monday, September 20, 2010


Chirp feed

Monday, September 20, 2010


chirpy.rb

get '/home' do
require_login
@myself = @user = User.get(session[:user])
@chirps = @user.chirp_feed
haml :home
end

get '/user/:id' do
require_login
@myself = User.get session[:user]
@user = User.first :chirpy_id => params[:id]
@chirps = @user.chirps
haml :home
end

Monday, September 20, 2010


Adding chirps

Monday, September 20, 2010


chirpy.rb

post '/update' do
require_login
@user = User.get session[:user]
@user.chirps.create :text => params[:chirp], :created_at =>
Time.now
redirect "/home"
end

Monday, September 20, 2010


Following users

Monday, September 20, 2010


chirpy.rb
get '/follow/:id' do
require_login
@myself = User.get session[:user]
@user = User.first :chirpy_id => params[:id]
unless @myself == @user or @myself.follows.include? @user
@myself.follows << @user
@myself.save
end
redirect '/'
end

get '/unfollow/:id' do
require_login
@myself = User.get session[:user]
@user = User.first :chirpy_id => params[:id]
unless @myself == @user
if @myself.follows.include? @user
follows = @myself.follows_relations.first :source => @user
follows.destroy
end
end
redirect '/'
end

Monday, September 20, 2010


View

Monday, September 20, 2010


Snippets

Sinatra does not have partial templates

Implement as helper

def snippet(page, options={})


haml page, options.merge!(:layout => false)
end

Monday, September 20, 2010


home.haml
=snippet :'snippets/top'

.span-16.append-1
=snippet :'snippets/update_box'
=snippet :'snippets/follow' if @myself
%h2 Home
=snippet :'snippets/chirps'

.span-7.last
=snippet :'snippets/info_box'

Monday, September 20, 2010


top.haml
.span-18
%a{:href => '/'}
%h2.banner Chirpy
.span-6.last
%a{:href => '/'} #{@myself.name}
|
%a{:href => '/'} home
|
%a{:href => '/logout'} logout

Monday, September 20, 2010


update_box.haml
=snippet :'/snippets/text_limiter_js'
%h2 What are you doing?
%form{:method => 'post', :action => '/update'}
%textarea.update.span-15#update{:name => 'chirp', :rows =>
2, :onKeyDown => "text_limiter($('#update'), $('#counter'))"}
.span-6
%span#counter
140
characters left
.prepend-12
%input#button{:type => 'submit', :value => 'update'}

Monday, September 20, 2010


text_limiter_js.haml
:javascript
function text_limiter(field,counter_field) {
limit = 139;
if (field.val().length > limit)
field.val(field.val().substring(0, limit));
else
counter_field.text(limit - field.val().length);
}

Monday, September 20, 2010


follow.haml
.span-15.last
- if @myself == @user
&nbsp;
- elsif @myself.follows.include? @user
You are following this user
|
%a{:href => "/unfollow/#{@user.chirpy_id}"} stop following this user
- else
%a{:href => "/follow/#{@user.chirpy_id}"} follow this user

Monday, September 20, 2010


chirps.haml
.chirps
-@chirps.each do |chirp|
%hr
.span-2
%img.span-2{:src => "#{chirp.user.photo_url}"}
.span-12
%a{:href => "/user/#{chirp.user.chirpy_id}"}
=chirp.user.name
&nbsp;
=chirp.text
.span-2.last
%a{:href =>"#", :onclick => "$('#update').attr('value','@#
{chirp.user.chirpy_id} ');$('#update').focus();"} reply
%br
%a{:href =>"#", :onclick => "$('#update').attr('value','RT @#
{chirp.user.chirpy_id}: #{chirp.text} ');$('#update').focus();"} rechirp

.span-15.last
%em.quiet
=time_ago_in_words(chirp.created_at)
%hr.space

Monday, September 20, 2010


info_box.haml
.span-2
%a
%img.span-2{:src => "#{@user.photo_url}"}
.span-3.last
.span-3
%em #{@user.name}
.span-3
#{@user.follows.count} following
.span-3
#{@user.followers.count} followers
.span-3
#{@user.chirps.count} chirps

%hr.space

.span-5.last
%h3 Follows
-@user.follows.each do |follow|
%a{:href => "/user/#{follow.chirpy_id}"}
%img.smallpic{:src => "#{follow.photo_url}", :width => '24px', :alt => "#{follow.name}"}

%hr.space

%h3 Followers
-@user.followers.each do |follower|
%a{:href => "/user/#{follower.chirpy_id}"}
%img.smallpic{:src => "#{follower.photo_url}", :width => '24px', :alt => "#{follower.name}"}

Monday, September 20, 2010


Questions?

Monday, September 20, 2010