ruby course - lesson 8 - build a simple twitter clone with ruby
TRANSCRIPT
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
User Session
Chirp
1 n
1
n
n
1
1 1
followers
follows
has
has
Monday, September 20, 2010
class Session include DataMapper::Resource property :id, Serial property :uuid, String, :length => 255 belongs_to :userend
class Friendship include DataMapper::Resource belongs_to :source, 'User', :key => true belongs_to :target, 'User', :key => trueend
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} endend
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 endend
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, trueset :show_exceptions, falseuse 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)
(3b) Facebook calls redirect URI with: - error reason
(3a) Facebook calls redirect URI with: - code
Monday, September 20, 2010
(1) Redirect user to /oauth/authorize with: - client id - redirect URI
(4) Go to /oauth/access_token with: - client id - redirect URI - client secret - code
(2) User authorizes (or not)
(3b) Facebook calls redirect URI with: - error reason
(3a) Facebook calls redirect URI with: - code
Monday, September 20, 2010
(1) Redirect user to /oauth/authorize with: - client id - redirect URI
(4) Go to /oauth/access_token with: - client id - redirect URI - client secret - code
(5) Facebook responds with: - access token - expiry
(2) User authorizes (or not)
(3b) Facebook calls redirect URI with: - error reason
(3a) Facebook calls redirect URI with: - code
Monday, September 20, 2010
(1) Redirect user to /oauth/authorize with: - client id - redirect URI
(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
(2) User authorizes (or not)
(3b) Facebook calls redirect URI with: - error reason
(3a) Facebook calls redirect URI with: - code
Monday, September 20, 2010
get '/login/facebook' do facebook_oauth_authorizeend
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
helper.rb
chirpy.rb
1
Monday, September 20, 2010
get "/#{FACEBOOK_OAUTH_REDIRECT}" do redirect_with_message '/login', params[:error_reason] if params[:error_reason] facebook_get_access_token(params[:code])end
chirpy.rb
Monday, September 20, 2010
get "/#{FACEBOOK_OAUTH_REDIRECT}" do redirect_with_message '/login', params[:error_reason] if params[:error_reason] facebook_get_access_token(params[:code])end
chirpy.rb
3a
Monday, September 20, 2010
get "/#{FACEBOOK_OAUTH_REDIRECT}" do redirect_with_message '/login', params[:error_reason] if params[:error_reason] facebook_get_access_token(params[:code])end
chirpy.rb
3a
3b
Monday, September 20, 2010
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
helper.rb
Monday, September 20, 2010
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
helper.rb
4
Monday, September 20, 2010
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
helper.rb
4 5
Monday, September 20, 2010
6
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
helper.rb
4 5
Monday, September 20, 2010
def get_user_from_facebook_with(token) JSON.parse RestClient.get "https://graph.facebook.com/me?access_token=#{token}" end
helper.rb
get '/logout' do @user = User.get session[:user] @user.session.destroy session.clear redirect '/'end
chirpy.rb
6
Monday, September 20, 2010
Authorization
Monday, September 20, 2010
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
helper.rb
Monday, September 20, 2010
Chirp feed
Monday, September 20, 2010
get '/home' do require_login @myself = @user = User.get(session[:user]) @chirps = @user.chirp_feed haml :homeend
get '/user/:id' do require_login @myself = User.get session[:user] @user = User.first :chirpy_id => params[:id] @chirps = @user.chirps haml :homeend
chirpy.rb
Monday, September 20, 2010
Adding chirps
Monday, September 20, 2010
post '/update' do require_login @user = User.get session[:user] @user.chirps.create :text => params[:chirp], :created_at => Time.now redirect "/home"end
chirpy.rb
Monday, September 20, 2010
Following users
Monday, September 20, 2010
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
chirpy.rb
Monday, September 20, 2010
View
Monday, September 20, 2010
Sinatra does not have partial templates
Implement as helper
def snippet(page, options={}) haml page, options.merge!(:layout => false) end
Snippets
Monday, September 20, 2010
=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'
home.haml
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 - 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 [email protected] 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 =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 [email protected] do |follow| %a{:href => "/user/#{follow.chirpy_id}"} %img.smallpic{:src => "#{follow.photo_url}", :width => '24px', :alt => "#{follow.name}"} %hr.space %h3 Followers [email protected] 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