├── .DS_Store ├── .gitignore ├── CHANGELOG ├── LICENSE ├── Manifest ├── README.rdoc ├── Rakefile ├── bin └── tunnel ├── lib └── frankie.rb └── test ├── frankie_test.rb └── helper.rb /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadprogram/frankie/0acfaf1f669f24858fa703c249657f7285961d82/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | pkg 3 | *.log -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.3.0 Updated to work with Sinatra 0.9.x and facebooker 1.0.2, thanks to mjfreshyfresh 2 | 3 | v0.2.5. Bugfix for fb_url_for helper method 4 | 5 | v0.2.4. Add fb_url_for helper method to create link URL's that have proper Facebook application reference included 6 | 7 | v0.2.3. Correct README and fix Sinatra version dependency in gem 8 | 9 | v0.2.2. Updated to deal with session change in Sinatra, plus misc bug fixes with Facebooker integration 10 | 11 | v0.2.0. Converted codebase to become Ruby Gem -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Ron Evans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | bin/tunnel 2 | CHANGELOG 3 | lib/frankie.rb 4 | LICENSE 5 | Manifest 6 | Rakefile 7 | README.rdoc 8 | test/frankie_test.rb 9 | test/helper.rb 10 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Frankie 2 | 3 | Frankie (http://facethesinatra.com) is a plugin for the Sinatra web framework (http://sinatrarb.com) that allows you to easily create a Facebook application by using the Facebooker gem. 4 | 5 | Written by Ron Evans (http://www.deadprogrammersociety.com) 6 | 7 | Based on merb_facebooker (http://github.com/vanpelt/merb_facebooker) by Chris Van Pelt, which was based on 8 | the Rails classes in Facebooker (http://facebooker.rubyforge.org/) by Mike Mangino, Shane Vitarana, & Chad Fowler 9 | 10 | 2/20/2009 - Now updated to Sinatra 0.9 and facebooker thanks to mjfreshyfresh. 11 | 12 | Thanks, everyone! 13 | 14 | = Here is a very simple example application: 15 | 16 | require 'rubygems' 17 | require 'frankie' 18 | 19 | configure do 20 | set_option :sessions, true 21 | load_facebook_config "./config/facebooker.yml", Sinatra.env 22 | end 23 | 24 | ## facebooker helpers 25 | before do 26 | ensure_authenticated_to_facebook 27 | ensure_application_is_installed_by_facebook_user 28 | end 29 | 30 | ## the site 31 | get '/' do 32 | body "

Hello #{session[:facebook_session].user.name} and welcome to frankie!

" 33 | end 34 | 35 | 36 | = How to use frankie 37 | - Install the frankie gem (which will install both Sinatra and Facebooker if you do not already have them) 38 | sudo gem install frankie 39 | 40 | - Create the application directories for your new app 41 | mkdir myapp 42 | cd myapp 43 | mkdir config 44 | 45 | - Put your facebooker.yml file into the /myapp/config directory, and set the values to your information. Here is a simple example of the file: 46 | 47 | development: 48 | api_key: apikeyhere 49 | secret_key: secretkeyhere 50 | canvas_page_name: yourcanvashere 51 | callback_url: http://localhost:4567 52 | test: 53 | api_key: apikeyhere 54 | secret_key: secretkeyhere 55 | canvas_page_name: yourcanvashere 56 | callback_url: http://localhost:4567 57 | production: 58 | api_key: apikeyhere 59 | secret_key: secretkeyhere 60 | canvas_page_name: yourcanvashere 61 | callback_url: http://yourrealserver.com 62 | 63 | 64 | - Make sure you have setup your Facebook application on the facebook site. Google "setup new facebook application" if you are unsure how to do this. I recommend starting with an IFrame application. A more advanced and cooler approach uses a tunneling script, which is included with Frankie. You do need to have "autossh" installed in order to use it, as well as a publicly addressable server. From a command prompt type tunnel like this: 65 | 66 | tunnel app.myhost.com 10000 4567 67 | 68 | You will also need to make sure your server's /etc/ssh/sshd_config contains the following line: 69 | 70 | GatewayPorts clientspecified 71 | 72 | Thanks to the many people like Evan Weaver, Blake Mizerany, and whoever else that have provided the code used in this tunneling script. 73 | 74 | - Create your application, based on the sample above, and then run it: 75 | ruby sample.rb 76 | 77 | - Test your app by going to http://apps.facebook.com/yourappname 78 | 79 | Have fun! -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | require 'echoe' 5 | 6 | task :default => :test 7 | 8 | Rake::RDocTask.new do |rd| 9 | rd.main = "README.rdoc" 10 | rd.rdoc_files += ["README.rdoc"] 11 | rd.rdoc_files += Dir.glob("lib/**/*.rb") 12 | rd.rdoc_dir = 'doc' 13 | end 14 | 15 | Rake::TestTask.new do |t| 16 | ENV['SINATRA_ENV'] = 'test' 17 | t.pattern = File.dirname(__FILE__) + "/test/*_test.rb" 18 | end 19 | 20 | Echoe.new("frankie") do |p| 21 | p.author = "Ron Evans" 22 | p.summary = "Easy creation of Facebook applications in Ruby using plugin for Sinatra web framework that integrates with Facebooker gem." 23 | p.url = "http://facethesinatra.com/" 24 | p.dependencies = ["sinatra >=0.2.2", "facebooker >=0.9.5"] 25 | p.install_message = "*** Frankie was installed ***" 26 | p.include_rakefile = true 27 | end 28 | 29 | -------------------------------------------------------------------------------- /bin/tunnel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | host, remote_port, local_port = ARGV 4 | raise 'You must specify at least a hostname - i.e. tunnel myhost.com' unless host 5 | remote_port ||= 10000 6 | local_port ||= 4567 7 | puts "Tunneling #{host}:#{remote_port} to 0.0.0.0:#{local_port}" 8 | begin 9 | exec "autossh -M 48484 -nNT -g -R *:#{remote_port}:0.0.0.0:#{local_port} #{host}" 10 | rescue 11 | raise "Tunnel failed to start. Do you have autossh installed?" 12 | end 13 | -------------------------------------------------------------------------------- /lib/frankie.rb: -------------------------------------------------------------------------------- 1 | # frankie - a plugin for sinatra that integrates with the facebooker gem 2 | # 3 | # written by Ron Evans (http://www.deadprogrammersociety.com) 4 | # 5 | # based on merb_facebooker (http://github.com/vanpelt/merb_facebooker) 6 | # and the Rails classes in Facebooker (http://facebooker.rubyforge.org/) from Mike Mangino, Shane Vitarana, & Chad Fowler 7 | # 8 | 9 | require 'sinatra' 10 | 11 | module Frankie 12 | 13 | module Loader 14 | 15 | require 'yaml' 16 | require 'uri' 17 | 18 | def load_facebook_config(file, env=:development) 19 | if File.exist?(file) 20 | yaml = YAML.load_file(file)[env.to_s] 21 | ENV['FACEBOOK_API_KEY'] = yaml['api_key'] 22 | ENV['FACEBOOK_SECRET_KEY'] = yaml['secret_key'] 23 | ENV['FACEBOOKER_RELATIVE_URL_ROOT'] = yaml['canvas_page_name'] 24 | end 25 | end 26 | 27 | end 28 | 29 | module EventContext 30 | 31 | # pin it to newer version of Facebooker 32 | gem 'mmangino-facebooker', '>=1.0.2' 33 | require 'facebooker' 34 | 35 | def facebook_session 36 | @facebook_session 37 | end 38 | 39 | def facebook_session_parameters 40 | {:fb_sig_session_key=>params["fb_sig_session_key"]} 41 | end 42 | 43 | def set_facebook_session 44 | session_set = session_already_secured? || secure_with_token! || secure_with_facebook_params! 45 | if session_set 46 | capture_facebook_friends_if_available! 47 | Facebooker::Session.current = facebook_session 48 | end 49 | session_set 50 | end 51 | 52 | def facebook_params 53 | @facebook_params ||= verified_facebook_params 54 | end 55 | 56 | private 57 | 58 | def session_already_secured? 59 | (@facebook_session = session[:facebook_session]) && session[:facebook_session].secured? 60 | end 61 | 62 | def secure_with_token! 63 | if params['auth_token'] 64 | @facebook_session = new_facebook_session 65 | @facebook_session.auth_token = params['auth_token'] 66 | @facebook_session.secure! 67 | session[:facebook_session] = @facebook_session 68 | end 69 | end 70 | 71 | def secure_with_facebook_params! 72 | return unless request_is_for_a_facebook_canvas? 73 | 74 | if ['user', 'session_key'].all? {|element| facebook_params[element]} 75 | @facebook_session = new_facebook_session 76 | @facebook_session.secure_with!(facebook_params['session_key'], facebook_params['user'], facebook_params['expires']) 77 | session[:facebook_session] = @facebook_session 78 | end 79 | end 80 | 81 | def create_new_facebook_session_and_redirect! 82 | session[:facebook_session] = new_facebook_session 83 | throw :halt, do_redirect(session[:facebook_session].login_url) unless @installation_required 84 | end 85 | 86 | def new_facebook_session 87 | Facebooker::Session.create(Facebooker::Session.api_key, Facebooker::Session.secret_key) 88 | end 89 | 90 | def capture_facebook_friends_if_available! 91 | return unless request_is_for_a_facebook_canvas? 92 | if friends = facebook_params['friends'] 93 | facebook_session.user.friends = friends.map do |friend_uid| 94 | Facebooker::User.new(friend_uid, facebook_session) 95 | end 96 | end 97 | end 98 | 99 | def blank?(value) 100 | (value == '0' || value.nil? || value == '') 101 | end 102 | 103 | def verified_facebook_params 104 | facebook_sig_params = params.inject({}) do |collection, pair| 105 | collection[pair.first.sub(/^fb_sig_/, '')] = pair.last if pair.first[0,7] == 'fb_sig_' 106 | collection 107 | end 108 | verify_signature(facebook_sig_params, params['fb_sig']) 109 | facebook_sig_params.inject(Hash.new) do |collection, pair| 110 | collection[pair.first] = facebook_parameter_conversions[pair.first].call(pair.last) 111 | collection 112 | end 113 | end 114 | 115 | # 48.hours.ago in sinatra 116 | def earliest_valid_session 117 | now = Time.now 118 | now -= (60 * 60 * 48) 119 | now 120 | end 121 | 122 | def verify_signature(facebook_sig_params,expected_signature) 123 | raw_string = facebook_sig_params.map{ |*args| args.join('=') }.sort.join 124 | actual_sig = Digest::MD5.hexdigest([raw_string, Facebooker::Session.secret_key].join) 125 | raise Facebooker::Session::IncorrectSignature if actual_sig != expected_signature 126 | raise Facebooker::Session::SignatureTooOld if Time.at(facebook_sig_params['time'].to_f) < earliest_valid_session 127 | true 128 | end 129 | 130 | def facebook_parameter_conversions 131 | @facebook_parameter_conversions ||= Hash.new do |hash, key| 132 | lambda{|value| value} 133 | end.merge( 134 | 'time' => lambda{|value| Time.at(value.to_f)}, 135 | 'in_canvas' => lambda{|value| !blank?(value)}, 136 | 'added' => lambda{|value| !blank?(value)}, 137 | 'expires' => lambda{|value| blank?(value) ? nil : Time.at(value.to_f)}, 138 | 'friends' => lambda{|value| value.split(/,/)} 139 | ) 140 | end 141 | 142 | def do_redirect(*args) 143 | if request_is_for_a_facebook_canvas? 144 | fbml_redirect_tag(args[0]) 145 | else 146 | redirect args[0] 147 | end 148 | end 149 | 150 | def fbml_redirect_tag(url) 151 | "" 152 | end 153 | 154 | def request_is_for_a_facebook_canvas? 155 | return false if params["fb_sig_in_canvas"].nil? 156 | params["fb_sig_in_canvas"] == "1" 157 | end 158 | 159 | def application_is_installed? 160 | facebook_params['added'] 161 | end 162 | 163 | def ensure_authenticated_to_facebook 164 | set_facebook_session || create_new_facebook_session_and_redirect! 165 | end 166 | 167 | def ensure_application_is_installed_by_facebook_user 168 | @installation_required = true 169 | authenticated_and_installed = ensure_authenticated_to_facebook && application_is_installed? 170 | application_is_not_installed_by_facebook_user unless authenticated_and_installed 171 | authenticated_and_installed 172 | end 173 | 174 | def application_is_not_installed_by_facebook_user 175 | throw :halt, do_redirect(session[:facebook_session].install_url) 176 | end 177 | 178 | def set_fbml_format 179 | params['format']="fbml" if request_is_for_a_facebook_canvas? 180 | end 181 | 182 | def fb_url_for(url) 183 | url = "" if url == "/" 184 | url = URI.escape(url) 185 | return url if !request_is_for_a_facebook_canvas? 186 | "http://apps.facebook.com/#{ENV['FACEBOOKER_RELATIVE_URL_ROOT']}/#{url}" 187 | end 188 | 189 | end 190 | 191 | end 192 | 193 | # extend sinatra with frankie methods, changed to work with Sinatra 0.9.x 194 | Sinatra::Default.send(:include, Frankie::EventContext) 195 | include Frankie::Loader -------------------------------------------------------------------------------- /test/frankie_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "frankie" do 4 | it "should be a placeholder for a real test" do 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require File.dirname(__FILE__) + '/../lib/frankie' 4 | require 'sinatra/test/spec' --------------------------------------------------------------------------------