sinbook 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,107 @@
1
+ sinbook: simple sinatra facebook extension in 300 lines of ruby
2
+ (c) 2009 Aman Gupta (tmm1)
3
+
4
+ === Usage
5
+
6
+ require 'sinbook'
7
+ require 'sinatra'
8
+
9
+ facebook do
10
+ api_key '4579...cbb0'
11
+ secret '5106...2342'
12
+ app_id 81747826609
13
+ url 'http://apps.facebook.com/myappname'
14
+ callback 'http://myappserver.com'
15
+ end
16
+
17
+ get '/' do
18
+ fb.require_login!
19
+ "Hi <fb:name uid=#{fb[:user]} useyou=false />!"
20
+ end
21
+
22
+
23
+ === Features
24
+
25
+ sinbook provides a simple `facebook` helper (also aliased to `fb`).
26
+
27
+ >> fb.valid?
28
+ => true # valid (authenticated) request from facebook's servers
29
+
30
+ >> pp fb.params
31
+ {
32
+ :logged_out_facebook => false, # request came from a user logged into facebook
33
+ :added => true, # user is logged into our app
34
+ :user => 1234, # user's facebook uid
35
+ :friends => [1,2,3], # list of user's friends
36
+ ...
37
+ }
38
+
39
+ >> fb[:user] # [] is aliased to params[]
40
+ => 1234
41
+
42
+ >> fb.callback
43
+ => 'http://apps.facebook.com/myappname'
44
+
45
+ >> fb.callback('/homepage')
46
+ => 'http://apps.facebook.com/myappname/homepage'
47
+
48
+ >> fb.url('/images/static.gif')
49
+ => 'http://myappserver.com/images/static.gif'
50
+
51
+ >> fb.appurl
52
+ => 'http://apps.facebook.com/add.php?api_key=4579...cbb0'
53
+
54
+ >> fb.addurl
55
+ => 'http://www.facebook.com/apps/application.php?id=81747826609'
56
+
57
+ >> fb.redirect('/welcome') # redirect using an fb:redirect tag
58
+
59
+ >> fb.require_login! # redirect to addurl page unless logged in
60
+
61
+
62
+ The helper can also be used to make API calls
63
+
64
+ >> fb.users.getInfo :uid => 1234, :fields => [:name]
65
+ => [{'uid' => 1234, 'name' => 'Frank Sinatra'}]
66
+
67
+ >> fb.groups.get :uid => 1234
68
+ => [{'name' => 'Sinatra Users'}]
69
+
70
+ >> fb.profile.setFBML :profile => 'hello world'
71
+ => true
72
+
73
+ >> fb.profile.getFBML
74
+ => 'hello world'
75
+
76
+
77
+ === Local Development
78
+
79
+ To develop locally, use ssh to setup a reverse tunnel to your external server.
80
+ For example, set your callback url to http://myserver.com:4567/, and run:
81
+
82
+ ssh -gNR 4567:localhost:4567 me@myserver.com
83
+
84
+ Then, simply launch sinatra on your local machine. Facebook will make requests to
85
+ http://myserver.com:4567/ which will be forwarded to port 4567 on your local machine.
86
+
87
+
88
+
89
+ === Other Options
90
+
91
+ facebook do
92
+ symbolize_keys true
93
+ end
94
+
95
+ >> fb.groups.get :uid => 1234
96
+ => [{:name => 'Sinatra Users'}]
97
+
98
+
99
+ === TODO
100
+
101
+ * Split out facebook api client so it can be used outside sinatra
102
+ * Add a batch mode for api calls:
103
+
104
+ groups, pics = fb.batch do |b|
105
+ b.groups.get :uid => 123
106
+ b.users.getInfo :uids => 123, :fields => [:pic_square]
107
+ end
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'sinbook'
3
+ require 'sinatra'
4
+
5
+ facebook do
6
+ api_key '858593842e5a3cefe59b72ddc7ffdd56'
7
+ secret '56ce1b26bf48ac8bac927c7d280b18f8'
8
+ app_id 185945096655
9
+ url 'http://tmm1.net:4568/'
10
+ callback 'http://tmm1.net:4568/'
11
+ end
12
+
13
+ set :port, 4568
14
+
15
+ get '/' do
16
+ haml :main
17
+ end
18
+
19
+ get '/receiver' do
20
+ %[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
21
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
22
+ <html xmlns="http://www.w3.org/1999/xhtml" >
23
+ <body>
24
+ <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js" type="text/javascript"></script>
25
+ </body>
26
+ </html>]
27
+ end
28
+
29
+ __END__
30
+
31
+ @@ layout
32
+ %html{:xmlns=>"http://www.w3.org/1999/xhtml", :'xmlns:fb'=>"http://www.facebook.com/2008/fbml"}
33
+ %head
34
+ %title Welcome to my Facebook Connect website!
35
+ %script{:type => 'text/javascript', :src => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php/en_US'}
36
+ %body
37
+ = yield
38
+ :javascript
39
+ FB.init("#{fb.api_key}", "/receiver")
40
+
41
+ @@ main
42
+ - if fb.valid? and fb[:user]
43
+ Hi,
44
+ %fb:profile-pic{:uid => fb[:user]}
45
+ %fb:name{:uid => fb[:user], :useyou => 'false', :firstnameonly => 'true'}
46
+ !
47
+ %br/
48
+ Do you want to <a href="javascript:FB.Connect.logoutAndRedirect('/')">logout</a>?
49
+ - else
50
+ Please login:
51
+ %fb:login-button{:onlogin => 'document.location.reload(true)'}
@@ -0,0 +1,60 @@
1
+ require 'rubygems'
2
+ require 'sinbook'
3
+ require 'sinatra'
4
+
5
+ facebook do
6
+ api_key '45796747415d12227f52146b4444cbb0'
7
+ secret '5106c7409f18d7618dd03433a2f72342'
8
+ app_id 81747826609
9
+ url 'http://apps.facebook.com/sinatrafacebook'
10
+ callback 'http://tmm1.net:4567'
11
+ end
12
+
13
+ get '/' do
14
+ if not facebook.valid?
15
+ # not accessed via facebook, redirect to the facebook app url
16
+ redirect fb.url
17
+
18
+ elsif fb[:logged_out_facebook]
19
+ # user is not logged into facebook
20
+ "Hey there! This is an awesome facebook app, but you must login to facebook to see it."
21
+
22
+ elsif not fb[:added]
23
+ # user is logged into facebook, but not our app
24
+
25
+ if not fb[:canvas_user]
26
+ # user navigated to the app directly, we know nothing about them
27
+ "
28
+ Hey there, you should add this <b>awesome</b> app! <br>
29
+ Go <a href='#{fb.addurl}'>here</a> or just click <a href='#' requirelogin=true>here</a>! <br>
30
+ Or, if you don't want to add the app, click <a href='#{fb.url('/')}'>here</a> so I know who you are.
31
+ "
32
+
33
+ else
34
+ # user came via a feed/notification or clicked on a link in the app, so we know who they are
35
+ "
36
+ Hey <fb:name uid=#{fb[:canvas_user]} useyou=false />. <br>
37
+ All I know about you is that you have #{fb[:friends].size} friends. <br>
38
+ Maybe you'll <a href='#{fb.addurl}'>add this app</a> so I can tell you more?
39
+ "
40
+ end
41
+
42
+ elsif fb[:user]
43
+ # logged into facebook and our app, we can get all their info
44
+ "
45
+ Hey <fb:name uid=#{fb[:user]} useyou=false />! <br>
46
+ Check out the special <a href='#{fb.url('/members')}'>members-only area</a>. <br>
47
+ And don't forget to tell your #{fb[:friends].size} friends to add this app too!
48
+ "
49
+ end
50
+ end
51
+
52
+ get '/members' do
53
+ fb.require_login!
54
+
55
+ groups = fb.groups.get :uid => fb[:user]
56
+ "
57
+ Hey there, now that you're a member I can tell what groups you're in on Facebook: <br>
58
+ #{groups.map{|g| g['name'] }.join('<br>')}
59
+ "
60
+ end
@@ -0,0 +1,355 @@
1
+ begin
2
+ require 'sinatra/base'
3
+ rescue LoadError
4
+ retry if require 'rubygems'
5
+ raise
6
+ end
7
+
8
+ module Sinatra
9
+ require 'digest/md5'
10
+ require 'yajl'
11
+
12
+ class FacebookObject
13
+ def initialize app
14
+ @app = app
15
+
16
+ @api_key = app.options.facebook_api_key
17
+ @secret = app.options.facebook_secret
18
+ @app_id = app.options.facebook_app_id
19
+ @url = app.options.facebook_url
20
+ @callback = app.options.facebook_callback
21
+ @symbolize_keys = app.options.facebook_symbolize_keys || false
22
+ end
23
+ attr_reader :app
24
+ attr_accessor :api_key, :secret
25
+ attr_writer :url, :callback, :app_id
26
+
27
+ def app_id
28
+ @app_id || self[:app_id]
29
+ end
30
+
31
+ def url postfix=nil
32
+ postfix ? "#{@url}#{postfix}" : @url
33
+ end
34
+
35
+ def callback postfix=nil
36
+ postfix ? "#{@callback}#{postfix}" : @callback
37
+ end
38
+
39
+ def addurl
40
+ "http://apps.facebook.com/add.php?api_key=#{self.api_key}"
41
+ end
42
+
43
+ def appurl
44
+ "http://www.facebook.com/apps/application.php?id=#{self.app_id}"
45
+ end
46
+
47
+ def require_login!
48
+ if valid?
49
+ redirect addurl unless params[:user]
50
+ else
51
+ app.redirect url
52
+ end
53
+ end
54
+
55
+ def redirect url
56
+ url = self.url + url unless url =~ /^http/
57
+ app.body "<fb:redirect url='#{url}'/>"
58
+ throw :halt
59
+ end
60
+
61
+ def params
62
+ return {} unless valid?
63
+ app.env['facebook.params'] ||= \
64
+ app.env['facebook.vars'].inject({}) do |h,(k,v)|
65
+ s = k.to_sym
66
+ case k
67
+ when 'friends'
68
+ h[s] = v.split(',').map{|e|e.to_i}
69
+ when /time$/
70
+ h[s] = Time.at(v.to_f)
71
+ when 'expires'
72
+ v = v.to_i
73
+ h[s] = v>0 ? Time.at(v) : v
74
+ when 'user', 'app_id', 'canvas_user'
75
+ h[s] = v.to_i
76
+ when /^(logged_out|position_|in_|is_|added)/
77
+ h[s] = v=='1'
78
+ else
79
+ h[s] = v
80
+ end
81
+ h
82
+ end
83
+ end
84
+
85
+ def [] key
86
+ params[key]
87
+ end
88
+
89
+ def valid?
90
+ if app.params['fb_sig'] # canvas/iframe mode
91
+ prefix = 'fb_sig'
92
+ vars = app.params
93
+ elsif app.request.cookies[api_key] # fbconnect mode
94
+ prefix = api_key
95
+ vars = app.request.cookies
96
+ else
97
+ return false
98
+ end
99
+
100
+ if app.env['facebook.valid?'].nil?
101
+ fbvars = {}
102
+ sig = Digest::MD5.hexdigest(vars.map{|k,v|
103
+ if k =~ /^#{prefix}_(.+)$/
104
+ fbvars[$1] = v
105
+ "#{$1}=#{v}"
106
+ end
107
+ }.compact.sort.join+self.secret)
108
+
109
+ if app.env['facebook.valid?'] = (vars[prefix] == sig)
110
+ app.env['facebook.vars'] = fbvars
111
+ end
112
+ end
113
+
114
+ app.env['facebook.valid?']
115
+ end
116
+
117
+ class APIProxy
118
+ Types = %w[
119
+ admin
120
+ application
121
+ auth
122
+ batch
123
+ comments
124
+ connect
125
+ data
126
+ events
127
+ fbml
128
+ feed
129
+ fql
130
+ friends
131
+ groups
132
+ links
133
+ liveMessage
134
+ notes
135
+ notifications
136
+ pages
137
+ photos
138
+ profile
139
+ sms
140
+ status
141
+ stream
142
+ users
143
+ video
144
+ ]
145
+
146
+ alias :__class__ :class
147
+ alias :__inspect__ :inspect
148
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
149
+ alias :inspect :__inspect__
150
+
151
+ def initialize name, obj
152
+ @name, @obj = name, obj
153
+ end
154
+
155
+ def method_missing method, opts = {}
156
+ @obj.request "#{@name}.#{method}", opts
157
+ end
158
+ end
159
+
160
+ APIProxy::Types.each do |n|
161
+ class_eval %[
162
+ def #{n}
163
+ (@proxies||={})[:#{n}] ||= APIProxy.new(:#{n}, self)
164
+ end
165
+ ]
166
+ end
167
+
168
+ def request method, opts = {}
169
+ if method == 'photos.upload'
170
+ image = opts.delete :image
171
+ end
172
+
173
+ opts = { :api_key => self.api_key,
174
+ :call_id => Time.now.to_f,
175
+ :format => 'JSON',
176
+ :v => '1.0',
177
+ :session_key => %w[ photos.upload ].include?(method) ? nil : params[:session_key],
178
+ :method => method }.merge(opts)
179
+
180
+ args = opts.map{ |k,v|
181
+ next nil unless v
182
+
183
+ "#{k}=" + case v
184
+ when Hash
185
+ Yajl::Encoder.encode(v)
186
+ when Array
187
+ if k == :tags
188
+ Yajl::Encoder.encode(v)
189
+ else
190
+ v.join(',')
191
+ end
192
+ else
193
+ v.to_s
194
+ end
195
+ }.compact.sort
196
+
197
+ sig = Digest::MD5.hexdigest(args.join+self.secret)
198
+
199
+ if method == 'photos.upload'
200
+ data = MimeBoundary
201
+ data += opts.merge(:sig => sig).inject('') do |buf, (key, val)|
202
+ if val
203
+ buf << (MimePart % [key, val])
204
+ else
205
+ buf
206
+ end
207
+ end
208
+ data += MimeImage % ['upload.jpg', 'jpg', image.respond_to?(:read) ? image.read : image]
209
+ else
210
+ data = Array["sig=#{sig}", *args.map{|a| a.gsub('&','%26') }].join('&')
211
+ end
212
+
213
+ ret = self.class.request(data, method == 'photos.upload')
214
+
215
+ ret = if ['true', '1'].include? ret
216
+ true
217
+ elsif ['false', '0'].include? ret
218
+ false
219
+ elsif (n = Integer(ret) rescue nil)
220
+ n
221
+ else
222
+ Yajl::Parser.parse(ret, :symbolize_keys => @symbolize_keys)
223
+ end
224
+
225
+ raise Facebook::Error, ret['error_msg'] if ret.is_a? Hash and ret['error_code']
226
+
227
+ ret
228
+ end
229
+
230
+ MimeBoundary = "--SoMeTeXtWeWiLlNeVeRsEe\r\n"
231
+ MimePart = %[Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n] + MimeBoundary
232
+ MimeImage = %[Content-Disposition: form-data; filename="%s"\r\nContent-Type: image/%s\r\n\r\n%s\r\n] + MimeBoundary
233
+
234
+ require 'resolv'
235
+ API_SERVER = Resolv.getaddress('api.facebook.com')
236
+ @keepalive = false
237
+
238
+ def self.connect
239
+ sock = TCPSocket.new(API_SERVER, 80)
240
+ begin
241
+ timeout = [3,0].pack('l_2') # 3 seconds
242
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout
243
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeout
244
+ rescue Exception => ex
245
+ # causes issues on solaris?
246
+ puts ex.inspect
247
+ end
248
+ sock
249
+ end
250
+
251
+ def self.request data, mime=false
252
+ if @keepalive
253
+ @socket ||= connect
254
+ else
255
+ @socket = connect
256
+ end
257
+
258
+ @socket.print "POST /restserver.php HTTP/1.1\r\n"
259
+ @socket.print "Host: api.facebook.com\r\n"
260
+ @socket.print "Connection: keep-alive\r\n" if @keepalive
261
+ if mime
262
+ @socket.print "Content-Type: multipart/form-data; boundary=#{MimeBoundary[2..-3]}\r\n"
263
+ @socket.print "MIME-version: 1.0\r\n"
264
+ else
265
+ @socket.print "Content-Type: application/x-www-form-urlencoded\r\n"
266
+ end
267
+ @socket.print "Content-Length: #{data.length}\r\n"
268
+ @socket.print "\r\n#{data}\r\n"
269
+ @socket.print "\r\n\r\n"
270
+
271
+ buf = ''
272
+ headers = ''
273
+ headers_done = false
274
+ chunked = true
275
+
276
+ while true
277
+ line = @socket.gets
278
+ headers << line unless headers_done
279
+ raise Errno::ECONNRESET unless line
280
+
281
+ if line == "\r\n" # end of headers/chunk
282
+ unless headers_done
283
+ headers_done = true
284
+ if headers =~ /Encoding: chunked/i
285
+ chunked = true
286
+ else
287
+ len = headers[/Content-Length: (\d+)/i,1].to_i
288
+ buf = @socket.read(len)
289
+ break # done!
290
+ end
291
+ end
292
+
293
+ line = @socket.gets # get size of next chunk
294
+ if line.strip! == '0' # 0 sized chunk
295
+ @socket.gets # read last crlf
296
+ break # done!
297
+ end
298
+
299
+ buf << @socket.read(line.to_i(16)) # read in chunk
300
+ end
301
+ end
302
+
303
+ buf
304
+ rescue Errno::EPIPE, Errno::ECONNRESET
305
+ @socket = nil
306
+ retry
307
+ ensure
308
+ @socket.close if @socket and !@keepalive
309
+ end
310
+ end
311
+
312
+ module FacebookHelper
313
+ def facebook
314
+ env['facebook.helper'] ||= FacebookObject.new(self)
315
+ end
316
+ alias fb facebook
317
+ end
318
+
319
+ class FacebookSettings
320
+ def initialize app, &blk
321
+ @app = app
322
+ @app.set :facebook_symbolize_keys, false
323
+ instance_eval &blk
324
+ end
325
+ %w[ api_key secret app_id url callback symbolize_keys ].each do |param|
326
+ class_eval %[
327
+ def #{param} val, &blk
328
+ @app.set :facebook_#{param}, val
329
+ end
330
+ ]
331
+ end
332
+ end
333
+
334
+ module Facebook
335
+ class Error < StandardError; end
336
+
337
+ def facebook &blk
338
+ FacebookSettings.new(self, &blk)
339
+ end
340
+
341
+ def self.registered app
342
+ app.helpers FacebookHelper
343
+ app.before(&method(:fix_request_method))
344
+ app.disable :sessions
345
+ end
346
+
347
+ def self.fix_request_method app
348
+ if method = app.request.params['fb_sig_request_method']
349
+ app.request.env['REQUEST_METHOD'] = method
350
+ end
351
+ end
352
+ end
353
+
354
+ Application.register Facebook
355
+ end
@@ -0,0 +1,24 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'sinbook'
3
+ s.version = '0.1.5'
4
+ s.date = '2009-11-25'
5
+ s.summary = 'simple sinatra facebook extension in 300 lines of ruby'
6
+ s.description = 'A full-featured facebook extension for the sinatra webapp framework'
7
+
8
+ s.homepage = "http://github.com/tmm1/sinbook"
9
+
10
+ s.authors = ["Aman Gupta"]
11
+ s.email = "aman@tmm1.net"
12
+
13
+ s.add_dependency('yajl-ruby')
14
+ s.has_rdoc = false
15
+
16
+ # ruby -rpp -e' pp `git ls-files | grep -v examples`.split("\n") '
17
+ s.files = [
18
+ "README",
19
+ "sinbook.gemspec",
20
+ "lib/sinbook.rb",
21
+ "examples/simple.rb",
22
+ "examples/connect.rb"
23
+ ]
24
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinbook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Aman Gupta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-25 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: yajl-ruby
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: A full-featured facebook extension for the sinatra webapp framework
26
+ email: aman@tmm1.net
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - sinbook.gemspec
36
+ - lib/sinbook.rb
37
+ - examples/simple.rb
38
+ - examples/connect.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/tmm1/sinbook
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.4
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: simple sinatra facebook extension in 300 lines of ruby
67
+ test_files: []
68
+